Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

A Simple Way to Pack your .NET Code into a Single Executable

4.69/5 (23 votes)
19 Feb 2008CPOL3 min read 1   2.4K  
C# and C++ source code for .NET application packer tool

Introduction

Applications built for .NET tend to be composed of a large number of assemblies and it's not a secret that .NET code can be easily decompiled using dissasemblers. It might be useful to pack all application assemblies into a single native executable, thus reducing application size and providing some degree of code protection. In this article, we'll look into a simple way of creating a .NET application packer tool. The complete source code and documentation of the tool is free to download at the Cellbi Web site.

Native Executable Template

First of all, we need to create a native executable, which we will use as a container for a target .NET application. We'll use C++ language for this purpose.

Since our template is a native application, we'll need to start .NET runtime manually from our code, then create a default AppDomain and then load our assemblies there. Only after that, it's OK to transfer execution to the managed code. Let's define the NativeLauncher class to handle this.

Here is the NativeLauncher definition:

C++
class NativeLauncher
{
public:
  NativeLauncher();
  int Launch(LPWSTR runtime, LPTSTR cmdLine);
};

NativeLauncher class contains only one method: Launch, it has two parameters runtime - to specify target .NET runtime version, and cmdLine - command line string passed.

Below is the code for the main method of our executable template:

C++
#define NET11 L"v1.1.4322";

HINSTANCE hInst;

int APIENTRY _tWinMain(HINSTANCE hInstance, 
    HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
  hInst = hInstance;

  LPWSTR runtime = NET11;

  NativeLauncher* launcher = new NativeLauncher();
  int result = launcher->Launch(runtime, lpCmdLine);
  delete launcher;
  launcher = NULL;
  return result;
}

We just create an NativeLauncher instance and call its Launch method, passing .NET runtime version and command line string.

Starting .NET Runtime

To start .NET runtime from NativeLauncher, we will use the CorBindToRuntimeEx function available from the mscorlib.tlb library. This function will bind to the specified .NET runtime and return a pointer to the ICorRuntimeHost interface. ICorRuntimeHost interface exposes the Start method, which we will use to start the .NET runtime.

Below is the sample code:

C++
CComPtr<ICorRuntimeHost> pHost;
HRESULT hr = CorBindToRuntimeEx(runtime,
    NULL,
    NULL,
    CLSID_CorRuntimeHost,
    IID_ICorRuntimeHost,
    (void **)&pHost);
if (FAILED(hr))
  return hr;

Note that CLSID_CorRuntimeHost, IID_ICorRuntimeHost are defined in the mscorlib.tlb library.

In order to use the CorBindToRuntimeEx function, we need to import the mscorlib.tlb library. Here is how to do this:

C++
#import <mscorlib.tlb> raw_interfaces_only

using namespace mscorlib;

Loading .NET Assemblies

Once we started the .NET runtime host, it is fine to load any .NET assembly into the default AddDomain and then execute our managed code. ICorRuntimeHost interface has the GetDefaultDomain method which can be used to obtain a pointer to the default AppDomain.

Below is the code illustrating this:

C++
CComPtr<IUnknown> pUnk;
hr = pHost->GetDefaultDomain(&pUnk);
if (FAILED(hr))
  return hr;

CComPtr<_AppDomain> appDomain;
hr = pUnk->QueryInterface(&appDomain.p);
if (FAILED(hr))
  return hr;

Now we can load the .NET assembly into the default AppDomain using any of the Load methods exposed by _AppDomain.

Here is the code:

C++
CComSafeArray<unsigned char> *rawAssembly = new CComSafeArray<unsigned char>();
rawAssembly->Add(packerApiLibLength, packerApiLib);

CComPtr<_Assembly> assembly;
hr = appDomain->Load_3(*rawAssembly, &assembly);
if (FAILED(hr))
  return hr;

At the code above, we load our assembly from an array or bytes. The array is stored in the packerApiLib variable.

Executing Managed Code

The next step is to create a .NET class and obtain its pointer. This is easy to do via the CreateInstance method exposed by the _Assembly interface. We just pass the full name of the class to create a pointer to result value:

C++
CComVariant launcher;
hr = assembly->CreateInstance(_bstr_t("Cellbi.AppPacker.Api.NetLauncher"), &launcher);
if (FAILED(hr))
  return hr;

if (launcher.vt != VT_DISPATCH)
  return E_FAIL;

We use CComVariant to store the pointer to newly created managed instance.

Ok, it's now time to call methods defined on the created instance, but we need to find the right method first:

C++
CComPtr<IDispatch> disp = launcher.pdispVal;
DISPID dispid;
OLECHAR FAR* methodName = L"Launch";
hr = disp->GetIDsOfNames(IID_NULL, &methodName, 1, LOCALE_SYSTEM_DEFAULT, &dispid);
if (FAILED(hr))
  return hr;

And now we call the method found. The Invoke method defined on the IDispatch interface does this:

C++
TCHAR szPath[MAX_PATH];
if (!GetModuleFileName(NULL, szPath, MAX_PATH))
  return E_FAIL;

CComVariant *path = new CComVariant(szPath);
CComVariant *cmdLineArg = new CComVariant(cmdLine);
CComVariant FAR args[] = {*cmdLineArg, *path};
DISPPARAMS noArgs = {args, NULL, 2, 0};
hr = disp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD,
  &noArgs, NULL, NULL, NULL);
if (FAILED(hr))
  return hr;

After the method is executed, we need to stop the .NET runtime:

C++
hr = pHost->Stop();
if (FAILED(hr))
  return hr;

That's all, I hope this article will be useful. Please let me know if there are any problems.

In this article, we didn't cover managed code used to pack .NET assemblies into the native executable template. The complete source code can be obtained at Cellbi.AppPacker.

History

  • December 5th, 2007 : Initial release
  • February 19th, 2008 : 1.0.0.20 build - Runtime version support added

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)