-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 createdthat 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. ILcreates 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 executableprogram, but one containing code for theothers 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

callvoid 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

callvoid [.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

callvoid [.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 indicatesthat 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

callvoid [.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 zzzextends [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 yyyextends [b]zzz

{

.method public hidebysig static void Main() il managed

{

.entrypoint

callvoid [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 yyyextends [b]zzz

{

.method public hidebysig static void Main() il managed

{

.entrypoint

callvoid abc()

ret

}

}

 

Error

Source file is ANSI

Creating PE file

Emitting members:

Global

Class 1Methods: 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 factThe 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 yyyextends [b]zzz

{

.method public hidebysig static void Main() il managed

{

.entrypoint

callvoid 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 zzzextends [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 yyyextends [a.dll]zzz

{

.method public hidebysig static void Main() il managed

{

.entrypoint

callvoid [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 zzzextends [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.