Last week I saw a question on our MSDN forums asking how to link C++ and C# code into a single binary. It's a scenario that we indeed support (albeit only from the command-line), but as I looked for a reference to which to point this person, I found precious little how-to in our own docs or on the web. I decided to whip up an example myself, and to my surprise it took me the better part of a whole afternoon to get right. The workflow is clearly more difficult than it should be, and I'm asking the team to look into how we can improve here in the future. Meanwhile, I'm going to walk through the example here to ensure there is some reference out there for Visual Studio 2005 developers.
Single binary interop
Generally, we recommend doing native/managed at the DLL boundary. When this pattern, you will typically have a collection of managed DLLs written in any managed language, a collection of native DLLs written in VC++, and a collection of mixed-mode DLLs that use C++/CLI to bridge between the two. However, occasionally you may need to compress these three layers into a single .exe or .dll in order to reduce dependencies, simplify deployment, or the like.
At a high level, the process for combining native and managed code into a single binary is logical: compile your source code into a collection of .netmodule and .objs and link them all together using the Visual C++ linker. However, the devil is in the details: dependencies between modules, linker switches, and where to use .objs versus .netmodules can be sticky issues.
An interop example
In my example, I'm going to create a C# application that uses C++/CLI to call into native C++ code without using C#'s unsafe language extensions. Let's take a look at the simple C# application in Listing 1, which consists mostly of the code generated for me by the new C# Console Application project wizard:
|Listing 1: Program.cs|
Two things worth noting here: this module is referencing a namespace called MyInteropCode, which I'll talk about in a moment, and it's calling a C++ function called CPPClass.CallCPPFcn(). Listing 2 contains the C++/CLI interop code that is being referenced by Program.cs.
|Listing 2: clrcode.cpp|
clrcode.cpp defines the MyInteropCode namespace, which contains a managed class called CPPClass. It is in this class (or some collection of similar classes) that I would place my interop functions, which leverage IJW interop technology to offer CLS-compliant interfaces and call native C++ within their implementation. In this case, I'm surfacing one static function called CallCPPFcn(), that returns a System::String. Within CallCPPFcn(), I create a local character array, pass that array to a native C++ function to be populated, and then construct a new System::String from that that array, which is returned to the C# caller in Program.cs. Listing 3 shows nativecode.cpp, which as you might guess contains the GetStringFromNative() function called from clrcode.cpp.
|Listing 3: nativecode.cpp|
GetStringFromNative()'s job is to populate the array passed in argument c with a locally defined string. The two C++ files share a common header, shown in Listing 4, which simply contains the prototype for GetStringFromNative().
|Listing 3: nativecode.h|
void GetStringFromNative(wchar_t* c, int num);
Building the example
To build an single binary interop app such as this, you'll need to compile in dependency order -- in this case that means the native code first, followed by the interop code, and then finally the purely managed code. As I mentioned earlier, Visual Studio 2005 doesn't support this build scenario, so you'll need to build from the command line. In this simple example we're talking about 4 commands: one to compile each of the source code files and one to link them all together. Let's look at these starting with native.cpp:
cl /c /MD nativecode.cpp
Pretty straightforward here; this compilation will generate an output file called nativecode.obj. clrcode.cpp is only slightly more complex:
cl /clr /LN /MD clrcode.cpp nativecode.obj
In this case I'm using the /clr switch to generate mixed-mode native/managed output and the /LN switch to indicate I wish to produce a clrcode.netmodule output target containing metadata as well as a clrcode.obj target containing code. Because this module contains a reference to the native code, I also need to pass nativecode.obj so that this file can be passed through to the link line when the compiler invokes the linker to produce the .netmodule. Program.cs is of course compiled using the C# command-line compiler:
csc /target:module /addmodule:clrcode.netmodule
In this case I'm using the /target switch to tell the C# compiler to product a Program.netmodule target and the /addmodule switch so that it can resolve references to my C++ interop code. I'm also passing the AssemblyInfo.cs file that was generated for me by the Console Application wizard, although this isn't a requirement.
Finally, I use the Visual C++ linker to link together the output generated by these three compilations into one executable file:
link /LTCG /CLRIMAGETYPE:IJW /ENTRY:ConsoleApplication1.Program.Main
/OUT:MixedApp.exe clrcode.obj nativecode.obj program.netmodule
There is quite a bit going on here, so let's look at each switch. The /LTCG switch enables link time code generation, which is required when linking in C#-generated .netmodules. The /ENTRY switch tells the linker what function to use as the program entry point. The /SUBSYSTEM switch is required when the linker can't use the entry point name to determine how to mark the executable. The /ASSEMBLYMODULE switch must include references the the C++-created .netmodules containing the interop code called from the managed code. The /OUT switch tells the linker the name of the output file. Finally, I pass the .obj files created by the VC++ compiler along with the .netmodules created by the C# compiler on the link line.
Now that it's built, running MixedApp.exe produces the very exciting output:
Hello from native C++!