Use reflection from native C++ code to run managed code

In the prior post (Use Reflection to create instances of objects) I showed how to create a plain C# console application that has no special references (including none to any WinForms assembly) that can load and invoke a WinForm.Exe program.

Today, we’ll look at doing the same thing from a plain old C++ native program. We need to

1. Start the CLR

2. get the default app domain

3. get the MSCorlib assembly loaded in that app domain

4. get the type of System.Reflection.Assembly from MSCorLib

5. invoke Assembly.LoadFrom, passing in the name of the WinForm.exe

6. from the result, get the Type of the WinForm we want to instantaite

7. Create an instance of the Type.

8. Invoke the “ShowDialog” method on the type

9. (when the Winform is closed) Stop the CLR

The code to execute these steps in C++ uses COM to communicate with the CLR, and demonstrates some of the power, but also shows some of the complexity.

The “#import <mscorlib.tlb>” line imports a standard TypeLibrary and generates a file “mscorlib.tlh” for TypeLibrary Header, which as 16,000 lines of COM interface definitions.

It’s easier to see these in a tool called OleView.

Start the VS Command prompt (or a standard command prompt and run this file:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\vc\bin\vcvars32.bat

Then type OleView<enter> to start the typelib viewer. Choose Menu->View TypeLib, then point it to “c:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.tlb”

Here OleView is showing some of the methods on _Assembly.

clip_image002

From this, we can see that the many managed types can be invoke via COM straight from plain old C++ code.

See also

Create and play your own Breakout game

Call managed code from your C++ code

Host the CLR and Generate IL to call a MessageBox

<code>

 // Win32Project6.cpp : Defines the entry point for the application.
//
/*
Start Visual Studio
File->New Project->C++->Win32->Win32 Project. Just click Finish on the Win32 Application Wizard to accept all the defaults.

After the #include lines, paste in the code below:

In _tWinMain, replace the TODO: Place code here. 
With  a call to our code:
    StartClrCode();

  Make sure to adjust the names of the assembly and type:
  (asmWinformFileName and typeNameToInstantiate)

Note: the default debugger is Native for this project, but because we start the CLR
  managed debugging is useful too.
    Project->Properties->Configuration Properties->Debugging->Debugger Type: Mixed
*/
#include "stdafx.h"
#include "Win32Project6.h"
#define MAX_LOADSTRING 100



#include "atlsafe.h"

#import <mscorlib.tlb> raw_interfaces_only rename("ReportEvent","ReportEventManaged")

#include <metahost.h>
#pragma comment(lib,"mscoree.lib")
#include <mscoree.h>
#include <comdef.h>

using namespace mscorlib;


// find a specified assembly from an AppDomain by enumerate all assemblies
HRESULT GetAssemblyFromAppDomain(_AppDomain* pAppDomain, LPCWSTR wszAssemblyName, _Deref_out_opt_ _Assembly **ppAssembly)
{
  *ppAssembly = NULL;
  // get the assemblies into a safearray
  SAFEARRAY *pAssemblyArray = NULL;
  HRESULT hr = pAppDomain->GetAssemblies(&pAssemblyArray);
  if (FAILED(hr)) 
  {
    return hr;
  }
  // put the safearray into a smart ptr, so it gets released
  CComSafeArray<IUnknown*>    csaAssemblies;
  csaAssemblies.Attach(pAssemblyArray);

  size_t cchAssemblyName = wcslen(wszAssemblyName);

  long cAssemblies = csaAssemblies.GetCount();
  for (long i=0; i<cAssemblies; i++)
  {
    CComPtr<_Assembly> spAssembly;
    spAssembly = csaAssemblies[i];
    if (spAssembly == NULL) 
      continue;
    CComBSTR cbstrAssemblyFullName;
    hr = spAssembly->get_FullName(&cbstrAssemblyFullName);
    if (FAILED(hr)) 
      continue;
    // is it the one we want?
    if (cbstrAssemblyFullName != NULL && 
      _wcsnicmp(cbstrAssemblyFullName, 
      wszAssemblyName, 
      cchAssemblyName) == 0)
    {
      *ppAssembly = spAssembly.Detach();
      hr = S_OK;
      break;
    }
  }
  if (*ppAssembly == 0)
  {
    hr = E_FAIL;
  }
  return hr;
}

void StartClrCode()
{
  CComPtr<ICLRMetaHost> spClrMetaHost;
  // get a MetaHost
  HRESULT hr = CLRCreateInstance(
    CLSID_CLRMetaHost, 
    IID_PPV_ARGS(&spClrMetaHost)
    );
  _ASSERT(hr == S_OK);

  // get a particular runtime version
  CComPtr<ICLRRuntimeInfo> spCLRRuntimeInfo;
  hr = spClrMetaHost->GetRuntime(L"v4.0.30319",
    IID_PPV_ARGS(&spCLRRuntimeInfo)
    );
  _ASSERT(hr == S_OK);

  // get the CorRuntimeHost
  CComPtr<ICorRuntimeHost> spCorRuntimeHost;
  hr = spCLRRuntimeInfo->GetInterface(
    CLSID_CorRuntimeHost,
    IID_PPV_ARGS(&spCorRuntimeHost)
    );
  _ASSERT(hr == S_OK);

  // Start the CLR
  hr = spCorRuntimeHost->Start();
  _ASSERT(hr == S_OK);

  // get the Default app domain as an IUnknown
  CComPtr<IUnknown> spAppDomainThunk;
  hr = spCorRuntimeHost->GetDefaultDomain(&spAppDomainThunk);
  _ASSERT(hr == S_OK);

  // convert the Appdomain IUnknown to a _AppDomain
  CComPtr<_AppDomain> spAppDomain;
  hr = spAppDomainThunk->QueryInterface(IID_PPV_ARGS(&spAppDomain));
  _ASSERT(hr == S_OK);

  // Get the mscorlib assembly
  CComPtr<_Assembly> sp_mscorlib;
  hr = GetAssemblyFromAppDomain(spAppDomain, L"mscorlib", &sp_mscorlib);
  _ASSERT(hr == S_OK);

  // the full file name on disk for the assembly
  auto asmWinformFileName = CComBSTR(L"C:\\Users\\calvinh\\Documents\\Visual Studio 2012\\Projects\\Paddle\\Paddle\\bin\\Debug\\Paddle.exe");
  // the name of the type to instantiate
  auto typeNameToInstantiate = CComBSTR(L"WindowsFormsApplication1.Form1");;
  //asmWinformFileName = CComBSTR(L"c:\\MemSpect\\Test\\csLife.exe");
  //typeNameToInstantiate =C ComBSTR(L"Life.Form1");

  // get the Type of "System.Reflection.Assembly"
  CComPtr<_Type> _typeReflectionAssembly;
  hr = sp_mscorlib->GetType_2(
    CComBSTR(L"System.Reflection.Assembly"),
    &_typeReflectionAssembly);
  _ASSERT(hr == S_OK);

  // create the array of args. only need 1 argument, array 
    auto psaLoadFromArgs = SafeArrayCreateVector(
      VT_VARIANT, 
      0, //start array at 0
      1); //# elems = 1
    long index=0;
    // set the array element
    CComVariant arg1(asmWinformFileName); // the argument: the asm to load
    SafeArrayPutElement(psaLoadFromArgs, &index, &arg1);

    //invoke the "Assembly.LoadFrom" public static member to load the paddle.exe
    CComVariant cvtEmptyTarget;
    CComVariant cvtLoadFromReturnValue;
    hr = _typeReflectionAssembly->InvokeMember_3(
      CComBSTR(L"LoadFrom"), 
      static_cast<BindingFlags>(BindingFlags_InvokeMethod |
      BindingFlags_Public | 
      BindingFlags_Static ),
      nullptr, //Binder
      cvtEmptyTarget, // target. Since the method is static, an empty variant
      psaLoadFromArgs, //args
      &cvtLoadFromReturnValue);
    _ASSERT(hr == S_OK);
    SafeArrayDestroy(psaLoadFromArgs); // don't need args any more
    _ASSERT(cvtLoadFromReturnValue.vt == VT_DISPATCH);

  // get the assembly from the return value
  CComPtr<_Assembly> srpAssemblyWinForm;
  srpAssemblyWinForm.Attach(
    static_cast<_Assembly *>(cvtLoadFromReturnValue.pdispVal
    ));

  // get the desired type from the assembly
  CComPtr<_Type> _typeForm;
  hr = srpAssemblyWinForm->GetType_2(
    typeNameToInstantiate, 
    &_typeForm);
  _ASSERT(hr == S_OK);

  // create an instance of the WinForm
  CComVariant resWinformInstance;
  hr = srpAssemblyWinForm->CreateInstance(
    typeNameToInstantiate, 
    &resWinformInstance);
  _ASSERT(hr == S_OK);

  // create an array var for return value of Type->GetMember
  SAFEARRAY *psaMember = nullptr;
  // Get the "ShowDialog" members to the array
  // (there are 2 overloads)
  // we use "ShowDialog" rather than the "Show"
  // because we want the Invoke to return only 
  // when the user closes the form.
  hr = _typeForm->GetMember(CComBSTR("ShowDialog"),
    MemberTypes_Method,
    (BindingFlags)( BindingFlags_Instance + BindingFlags_Public),
    &psaMember); 
  _ASSERT(hr == S_OK);

  // put into SafeArray so it gets released
  CComSafeArray<IUnknown *> psaMem;
  psaMem.Attach(psaMember);

  // Get the Method Info for "ShowDialog" from the 1st type in the array
  CComPtr<_MethodInfo> methodInfo;
  hr = psaMem[0]->QueryInterface(
    IID_PPV_ARGS(&methodInfo)
    );
  _ASSERT(hr == S_OK);

  // invoke the ShowDialog method on the Form
  hr = methodInfo->Invoke_3(
    resWinformInstance, 
    nullptr, //parameters
    nullptr // return value
    );
  _ASSERT(hr == S_OK);

  // stop the runtime
  hr = spCorRuntimeHost->Stop();
  _ASSERT(hr == S_OK);
}

// Global Variables:
HINSTANCE hInst;                             // current instance
TCHAR szTitle[MAX_LOADSTRING];                 // The title bar text
TCHAR szWindowClass[MAX_LOADSTRING];           // the main window class name

// Forward declarations of functions included in this code module:
ATOM             MyRegisterClass(HINSTANCE hInstance);
BOOL             InitInstance(HINSTANCE, int);
LRESULT CALLBACK   WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK   About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
                       _In_opt_ HINSTANCE hPrevInstance,
                       _In_ LPTSTR    lpCmdLine,
                       _In_ int       nCmdShow)
{
  UNREFERENCED_PARAMETER(hPrevInstance);
  UNREFERENCED_PARAMETER(lpCmdLine);

  StartClrCode();

  MSG msg;
  HACCEL hAccelTable;

  // Initialize global strings
  LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
  LoadString(hInstance, IDC_WIN32PROJECT6, szWindowClass, MAX_LOADSTRING);
  MyRegisterClass(hInstance);

  // Perform application initialization:
  if (!InitInstance (hInstance, nCmdShow))
  {
    return FALSE;
  }

  hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WIN32PROJECT6));

  // Main message loop:
  while (GetMessage(&msg, NULL, 0, 0))
  {
    if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
    {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
  }

  return (int) msg.wParam;
}

//
//  FUNCTION: MyRegisterClass()
//
//  PURPOSE: Registers the window class.
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
  WNDCLASSEX wcex;

  wcex.cbSize = sizeof(WNDCLASSEX);

  wcex.style            = CS_HREDRAW | CS_VREDRAW;
  wcex.lpfnWndProc  = WndProc;
  wcex.cbClsExtra       = 0;
  wcex.cbWndExtra       = 0;
  wcex.hInstance        = hInstance;
  wcex.hIcon            = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WIN32PROJECT6));
  wcex.hCursor      = LoadCursor(NULL, IDC_ARROW);
  wcex.hbrBackground    = (HBRUSH)(COLOR_WINDOW+1);
  wcex.lpszMenuName = MAKEINTRESOURCE(IDC_WIN32PROJECT6);
  wcex.lpszClassName    = szWindowClass;
  wcex.hIconSm      = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

  return RegisterClassEx(&wcex);
}

//
//   FUNCTION: InitInstance(HINSTANCE, int)
//
//   PURPOSE: Saves instance handle and creates main window
//
//   COMMENTS:
//
//        In this function, we save the instance handle in a global variable and
//        create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
  HWND hWnd;

  hInst = hInstance; // Store instance handle in our global variable

  hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

  if (!hWnd)
  {
    return FALSE;
  }

  ShowWindow(hWnd, nCmdShow);
  UpdateWindow(hWnd);

  return TRUE;
}

//
//  FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  PURPOSE:  Processes messages for the main window.
//
//  WM_COMMAND - process the application menu
//  WM_PAINT   - Paint the main window
//  WM_DESTROY - post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
  int wmId, wmEvent;
  PAINTSTRUCT ps;
  HDC hdc;

  switch (message)
  {
  case WM_COMMAND:
    wmId    = LOWORD(wParam);
    wmEvent = HIWORD(wParam);
    // Parse the menu selections:
    switch (wmId)
    {
    case IDM_ABOUT:
      DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
      break;
    case IDM_EXIT:
      DestroyWindow(hWnd);
      break;
    default:
      return DefWindowProc(hWnd, message, wParam, lParam);
    }
    break;
  case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    // TODO: Add any drawing code here...
    EndPaint(hWnd, &ps);
    break;
  case WM_DESTROY:
    PostQuitMessage(0);
    break;
  default:
    return DefWindowProc(hWnd, message, wParam, lParam);
  }
  return 0;
}

// Message handler for about box.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
  UNREFERENCED_PARAMETER(lParam);
  switch (message)
  {
  case WM_INITDIALOG:
    return (INT_PTR)TRUE;

  case WM_COMMAND:
    if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
    {
      EndDialog(hDlg, LOWORD(wParam));
      return (INT_PTR)TRUE;
    }
    break;
  }
  return (INT_PTR)FALSE;
}

</code>