C/C++ Users Journal November, 2004
Sometimes it is essential for a program to load and execute another module dynamically. The external module may be started as a separate process running in its own address space, or loaded and executed as a shared library or a DLL in the address space of the calling process. Consider this code, which works on most flavors of UNIX:
typedef int (*DFUNC) (const char *);
// Load the library
void* handle = dlopen
("libDTest.so", RTLD_LAZY);
// Retrieve a function pointer by name
DFunc dfp = (DFUNC) dlsym
(handle, "functionName");
// Call the function
int r = (*dfp) ("do something");
// Unload the library
dlclose (handle);
Calling dlclose does not guarantee that the shared library objects associated with the handle are removed from the address space, especially if all references to the handle are not yet released.
Windows has a similar mechanism to dynamically load/unload external software modules:
typedef int (*DFUNC) (const char *);
HINSTANCE handle =
LoadLibraryEx("libDTest", NULL, 0);
DFUNC dfp = (DFUNC) GetProcessAddress
(handle, "functionName");
int r = dfp("do something");
This code does not prevent the system from keeping a file handle on the library, making it impossible to replace it by a new version. This is a problem when the calling process has to replace the called module.
Java class loaders load classes dynamically into a running module. Unloading loaded classes is not straightforward and is dependent on the implementation of the JVM.
Another issue is that, in all these cases except Java, the loaded executable runs with the same privileges as the loading program. This may have been fine in the pre-Internet days, but in current operating environments, this may be a grave security violation.
The .NET Framework provides a clean and unambiguous way of loading/unloading external modules. In addition, it lets the loading program create a security "sandbox" in which the loaded program executes. The privilege bestowed upon the loaded program is a subset of the privileges of the loading program.
We illustrate these concepts using this program workflow:
Before embarking upon the task of dynamically compiling .NET code, we present CodeBuilder, a C++ class (Listing 1) that returns a string containing C# source code for the MyFunc class. Part of the class source code is inserted from the user's input (line 18) passed as a string pointer (line 7.)
Listing 2 is the C++ CodeCompiler class, which performs the dynamic compilation. The class has two methods: The CompileCode method is responsible for compiling the code and loading it into memory. The Invoke method is used to invoke the MyFunc::f function using reflection.
Line 17 creates the C# compiler instance. Lines 20-22 set the parameters for the compiler: The notable option is the GenerateInMemory = true statement, which forces compilation into memory. This avoids storing the assembly on the hard drive.
Lines 25-26 lower the privileges of the compiled code to that of an assembly downloaded from the Internet. This is done by changing the code's "evidence." In .NET, the evidence classifies the application into so-called code groups: To belong to a code group, the code has to bring the evidence required by the group, be it a location on the filesystem, a digital signature, a code origination, a URI, or some custom evidence. Code groups are categories, each of which is mapped to a "permission set." The code groups are the glue between the evidence and the permission sets.
For example, an assembly downloaded from a network share would end up in the Intranet code group. Its evidence would be SecurityZone::Intranet. In our case, we override the code evidence to SecurityZone::Internet, thereby mapping it to the Internet code group. This grants it Internet permission set privileges and prevents the assembly's access to almost all computer resources. We could also create our own custom code group, which we would bind to some custom permission set.
Line 28 compiles the code. The code assembly is stored in the oAssembly_ data member.
The Invoke method uses reflection to call the method. This method is a prime example of the .NET Framework code interoperability. The user's code may have been Visual Basic .NET or Fujitsu NetCOBOL for .NET and it would be called in the same manner: Only line 18 would be changed to a different CodeProvider.
When we started this project, we wanted to get some user input (in this case, a mathematical function written in C#) and perform some operations on that user's input. Listing 3 is a trivial implementation of these requirements.
As you can see, the execution of the computations, which are based on the various functions entered by the user, are delegated to the Execute(String* f) method. This method could follow a simple strategy: We would pass the user's input to the CodeBuilder class, pass the resulting code to the CodeCompiler class, and run the f method of the resulting object. Unfortunately, this strategy has a drawback: To run the resulting object, it has to be loaded in the same application domain as the CodeCompiler.
The computer science concept closest to application domains is probably sandboxing. An application domain is an isolated environment where code executes. Application domains provide isolation for executing managed code. They are also in charge of loading the code prior to its execution. In general, before running a typical application, the default application domain loads all the assemblies referenced by this application. This guarantees that the code is isolated inside of the application domain, providing an enhanced tolerance to failures and customized security permissions. Although application domains, represented by AppDomain objects, could be thought of as processes, the comparison is misleading as multiple application domains can run in a single process. Application domains are not akin to threads either: A single application domain can contain several threads and threads are not confined to a single application domain and can create new application domains.
By design, the CLR cannot unload an assembly from an application domain without tearing down the entire application domain itself. This means that if we decided to run the CodeCompiler or the user's code in the same application domain as the _tmain() or Execute(String* f) methods, it would be impossible to unload the code generated from the user's input without ending the program itself.
We must resort to a stratagem to be able to get more than one user's input. We need to run the CodeCompiler and the user's code in an AppDomain distinct from the default AppDomain. Once we are done with the code, we can simply free up this additional resource, thereby unloading the user's code. To this end, we call the CreateDomain method of the default AppDomain instance to create a new application domain. Once we do not need this extra AppDomain anymore, it will be unloaded; see Figure 1.
To achieve similar results without the .NET Framework, we would need to start a new process running as a least-privileged user. However, the marshaling between the two processes and managing their lifecycle would be much more complicated than managing the two .NET application domains.
Before we can instantiate a CodeCompiler instance in the new "CodeCompilerDomain" AppDomain, we need to load the code of the CodeCompiler assembly in a byte array. This is done using the function in Listing 4.
We are now ready to perform the following operations in the Execute(String* f) function (Listing 5):
Because application domains are the smallest unit of code unloading in .NET, we call the static Unload() method at the end of the Execute(String *) function. The method sends ThreadAborts exceptions to each thread still running in the AppDomain, frees up all memory structures from the AppDomain assemblies, prevents threads from entering the AppDomain, and raises the DomainUnload event. This step takes care of the CodeCompiler object as well as the user's code. We are ready to accept a new round of user's inputs without the risk of filling up the memory with unneeded assemblies.
We are ready to run the program. At the prompt, enter a function and the program returns 10 values for points between 0 and 2
:
Enter f(x) = Sin(x) f(0) = 0 f(0.5) = 0.479425538604203 f(1) = 0.841470984807897 f(1.5) = 0.997494986604054 f(2) = 0.909297426825682 f(2.5) = 0.598472144103957 f(3) = 0.141120008059867 f(3.5) = -0.35078322768962 f(4) = -0.756802495307928 f(4.5) = -0.977530117665097 f(5) = -0.958924274663138 f(5.5) = -0.705540325570392 f(6) = -0.279415498198926
If the user tries entering malicious code, such as Sin(x); System.IO.File.Delete("Test.txt") or Sin(x); Microsoft.Win32.Registry.LocalMachine.CreateSubKey("SOFTWARE\\A.Test") to trick the application into executing code that could be harmful, the .NET runtime prevents its execution; see Figure 2.
For C/C++ programmers, .NET offers a number of features that simplify a number of nontrivial programming tasks. In this article, we have examined some of the central aspects of this technology such as dynamic compilation, highly granular security, language interoperability, and code isolation. Without .NET, a program with the features described here would be much harder to implement in C/C++.