-14-
External DLLs
So far, all our code has been
contained in one single IL file. This is not practical because, in real life
projects, hundreds of people work together, and the code they write is placed
in different files that must be shared or used by others.
a.il
.module a.dll
.class private auto autochar zzz
{
.method public hidebysig static void abc() il managed
{
ldstr "Hi"
call void System.Console::WriteLine(class System.String)
ret
}
}
In the above program, a class
zzz is created that resides in a file
called a.il. It contains a single function abc. The class is private and the
function is public since we want to enable other programs to call the code
located in our class. When we compile the above program i.e. run ilasm on a.il with
the dll option:
>ilasm a.il /dll
The assembler creates a file
with a .dll extension and not an exe file.
A dll is used under Windows to
store code that other programs can call. As we are not specifying an executable
or a stand-alone program, we have no directive called assembly. Instead, we
have used the directive module, which is given the name of the dll. This
directive is optional. IL creates one
for us automatically, defaulting to the name of the output file, if we don’t
specify one. It is good idea to tell the world this is not an executable program, but one containing code for
the others to use.
We would now like to call this
function abc from class zzz, which is located in a.il from a function in b.il.
b.il
.assembly mukhi {}
.class public auto autochar yyy
{
.method public static hidebysig void Main() il managed {
.entrypoint
call void zzz::abc()
ret
}
}
We assemble this program as
before. As usual we start with the directive assembly. As stated earlier, we
are planning to call a function abc from class zzz. When we assemble the
program, we do not get any error, but when we run the program, we get the
following exception:
Output
Exception occurred: System.TypeLoadException: Could not load
class 'zzz'.
at yyy.Main()
The runtime cannot load the
class zzz since it does not reside in the current directory. If you remember,
this function was created in a module called a.dll. Let us get back to the
drawing board and supply this piece of information to the assembler.
b.il
.assembly mukhi {}
.class public auto autochar yyy
{
.method public static hidebysig void Main() il managed {
.entrypoint
call void [.module
a.dll]zzz::abc()
ret
}
}
Error
***** FAILURE *****
Oops! An error has been
generated. If you remember, sometime ago, we have prefaced the name of the
class with the name of the dll file that contained the code. We thought of
doing the same in the above program, but this caused an error. However, the
assembler does not tell us where the error is. Most of the time it behaves in
such a secretive manner and keeps the line numbers of the code where the error
has occurred, close to its chest.
b.il
.assembly mukhi {}
.module extern a.dll
.class public auto autochar yyy
{
.method public static hidebysig void Main() il managed {
.entrypoint
call void [.module
a.dll]zzz::abc()
ret
}
}
Now the assembler error
disappears, but the runtime generates an exception
Output
Exception occurred: System.TypeLoadException: Could not load
class 'zzz'.
at yyy.Main()
Whenever we use the module
directive in front of a function, the module must be declared earlier. This is
done, using the same module directive with two parameters, i.e. extern and the
name of the module.
The extern indicates that some of the code that we will be using
later will reside in the file a.dll. Thus, if we do not declare a module as
extern earlier in our file, we cannot use it later to signify that the code
comes from this module. However we still get an error at runtime, saying that
the class could not be loaded.
b.il
.assembly mukhi {}
.file a.dll
.module extern a.dll
.class public auto autochar yyy
{
.method public static hidebysig void Main() il managed {
.entrypoint
call void [.module a.dll]zzz::abc()
ret
}
}
Output
Hi
Now everything works as
expected. This is because, we added a directive file, that informed the runtime
to load the file a.dll in memory, as it contains some code that we are
referring to. This class zzz could contain numerous functions and fields. This
is how we access the WriteLine function from the Console class.
Let us now explain a fundamental
concept that the .NET world has introduced.
a.cs
public class zzz
{
public static void abc()
{
System.Console.WriteLine("bye");
}
}
We compile the above C# program
as
csc /target:library a.cs
The above line produces a file
called a.dll. When we run the same program b.exe, the string "bye" is
displayed. The point that we want to make is that, we can call code from a dll
without being concerned whether the code was written in C# or in any other
programming language. Thus, we have no way of knowing as to which language the
code of the WriteLine function from the class Console has been written in.
This is how, the .NET world puts
a stop to all debates on which programming is better. Finally, all the code is
converted into IL code in the .Net world.
Let us now modify b.il to create
a dll.
b.il
.assembly b {}
.module b.dll
.class public auto ansi zzz
extends [mscorlib]System.Object
{
.method public hidebysig static void abc() il managed
{
ldstr
"abc"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
We compile this file to a dll by
running
ilasm b.il /dll
This creates a file called
b.dll. The only change that we have introduced is the addition of an assembly
directive. Other than that, everything else remains the same.
Then we have created a file
called c.il as follows:
c.il
.assembly extern b {}
.assembly c {}
.class public auto ansi yyy
extends [b]zzz
{
.method public hidebysig static void Main() il managed
{
.entrypoint
call void [b]zzz::abc()
ret
}
}
Output
abc
We assemble it as normal. When
we run c.exe, abc is displayed. Here, we are deriving from the class zzz that
is present in the dll b. Whenever we derive from another class, we have to add
the assembly directive in the file c.il. Otherwise, the following error is
displayed when the program runs:
Output
Exception occurred: System.TypeLoadException: Could not load
class 'zzz'.
Exception occurred: System.MissingMethodException: Could not
find the entry point.
c.il
.assembly extern b {}
.assembly c {}
.class public auto ansi yyy
extends [b]zzz
{
.method public hidebysig static void Main() il managed
{
.entrypoint
call void abc()
ret
}
}
Error
Source file is ANSI
Creating PE file
Emitting members:
Global
Class 1 Methods: 1;
Resolving member refs:
***** FAILURE *****
The function abc may lie in the
class zzz, from which the class yyy has been derived, but we have to explicitly
inform the assembler as to which class the function should be called from. The
compiler stays ignorant of this fact
The point is that file b.dll is not parsed to check for functions
contained in it.
c.il
.assembly extern b {}
.assembly c {}
.class public auto ansi yyy
extends [b]zzz
{
.method public hidebysig static void Main() il managed
{
.entrypoint
call void zzz::abc()
ret
}
}
The above program generates no
error when we assemble it, but when we run the program, we get the following
exception:
Output
Exception occurred: System.TypeLoadException: Could not load
class 'zzz'.
at yyy.Main()
No assumptions are made in the
IL world. You have to explicitly state each and every time that, the class zzz
is located in module b.dll. Stating it once is not enough. Now you know why it
is better for programs to generate IL code. There is too much repetition.
Now, modify a.il to create a dll
and b.il to create an executable.
a.il
.assembly a.dll {}
.module a.dll
.class public auto ansi zzz
extends [mscorlib]System.Object
{
.method public hidebysig static void abc() il managed
{
ldstr
"abc"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
b.il
.assembly extern a.dll {}
.assembly b {}
.class public auto ansi yyy
extends [a.dll]zzz
{
.method public hidebysig static void Main() il managed
{
.entrypoint
call void
[a.dll]zzz::abc()
ret
}
}
Output
abc
The only change made here is
that, we have called the assembly a.dll and hence, we place the same names in
the [] brackets. Also, we are not allowed to specify .module in the []
brackets. We also do not have a .file directive in our file b.il, like we had earlier.
a.il
.assembly a.dll {}
.module a.dll
.class private auto ansi zzz
extends [mscorlib]System.Object
{
.method public hidebysig static void abc() il managed
{
ldstr
"abc"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
We have made one small change in
the file a.il only. The file b.il remains the same. When we run b.exe, we get
the following exception.
Output
Exception occurred: System.MethodAccessException: zzz.abc()
at yyy.Main()
The reason for the above error
is that, the class zzz has been made private and hence cannot be accessed from
outside. Private is the most restrictive access modifier.
We then changed the access
modifier of the class to public, but made the access modifier of the function
to private. On doing so, we get the same exception again. Thus, both the class
and the function must be public, if the class has to be accessible from the
outside.
We have exactly seven
accessibility types. We have touched upon two of them, public and private. We
will now change the access modifier from private to family. Thereafter, no
error will be generated since classes derived from zzz are allowed to call the
function. But, on replacing family by assembly an error is generated since only
the same assembly is allowed to call the function. Then, we have variants on
the above two. They are famandassem and famorassem, that are true if either
both the conditions are true or only one is true. The last one is privatescope.