次の方法で共有


Write native to managed interop code: reg free COM

If you followed the steps of the last post (Call C# code from your legacy C++ code), then your C++ program simply started the CLR (Common Language Runtime), invoked a C# method that returned the length of the passed in string argument. Not very exciting.

Let’s spice it up a little.

We’d like to allow the native code to call the managed code in a well-defined way. Interfaces come to mind. COM (the Component Object Model) was designed to be a way to have software communicate in a language independent way. COM gives a way to describe the software capabilities of one library that can be used by a totally different program, in a different language. These descriptions are stored in something called a Type Library, which is a language independent way of describing software. A Type Library holds, among other things, definitions for Interfaces.

COM Interfaces can be implemented and used in many languages. They can be defined and implemented in FoxPro, VB, C# and C++, etc. and COM glues it all together.

What is an Interface? It’s a way of interacting with something. For example, imagine how you interact with a table lamp. You can imagine the interface to  contain methods, and properties:

public interface ILamp

    {

bool IsOn { get;}

void TurnOn();

void TurnOff();

    }

The description of the interface is stored in a generated Type Library and registered on a computer in the registry, so that it can be discovered by any other software on the machine.

All COM interfaces inherit from a single interface, called IUnknown, the unknown interface. It only has 3 methods on it:

QueryInterface, Addref, and Release. The last 2 just increment and decrement a count. When the count goes to zero, the object can be deleted.

QueryInterface is a way to ask the object if it implements an interface: It can succeed or fail. If success, then an object that implements the specified interface is returned. Thus, there is no way to call a method on an object using an interface that it doesn’t implement.

I like to use this analogy. You’re taking a walk. You’re waiting at a street corner to cross the street.  Somebody comes up next to you, also waiting. You can use the IRandomPerson interface analog to IUnknown which has only 2 methods:

Interface IRandomPerson

{

               What time is it?

              Talk about the weather

CanYouDirectMeTo(Place destination)

WhatIsHeWearing {get;}

}

Now suppose you say “Great weather today”. You get a response, “yes, but it was really bad in France last week”

Now you can safely use the new method :IFrance because you know that person  knows something about Boston

Interface IFrance

{

                Talk about Paris,

                Talk about the French,

}

Or you could use the WhatIsHeWearing property and see that she has a Seahawks cap on. Then you can use the ISeahawk interface:

Interface ISeahawk

{

Commiserate on last 30 seconds of superbowl

Will they have a winning season?

}

You get the idea.

Let’s create a Type Library for the C# project.

Right click on the C# ClassLibrary1 project->Properties->Build->(Configuration: All configurations) Register for COM interop

Now when you build, you might get an error message

error MSB3213: Cannot register type library "c:\users\calvinh\documents\visual studio 2013\Projects\StartClr\Debug\ClassLibrary1.tlb". Error accessing the OLE registry. (Exception from HRESULT: 0x8002801C (TYPE_E_REGISTRYACCESS))

You’ll need to run Visual Studio as Administrator so the registry can be modified to register the Type Library. Don’t worry, the finished product will run as a non-Admin.

Now let’s create an interface:

Put in this code:

   [ComVisible(true)]

    [Guid("535C044D-EA08-4DC9-8E90-8520B47B65BC")]

public interface IMyInterface

    {

string GetAStringFromParameters(string strParam, int intParam);

    }

The Guids (Globally Unique Identifiers) are created from a tool (Tools->Create-GUID).

clip_image001

Why do we need Guids? It’s a way of making absolutely sure that there is only 1 interface called IMyInterface. Also, if you don’t specify a GUID, the guids will be generated for you, and could change from build to build, littering your registry with orphaned entries.

We want the ClassLibrary1 project to build before the C++ code, which depends on it, so right click on the Solution, Project Dependencies:

image

In the Solution Explorer ClassLibrary1->Properties->AssembyInfo.Cs file, you’ll see something like this:

// The following GUID is for the ID of the typelib if this project is exposed to COM

[assembly: Guid("a8550677-2f41-4071-8363-b05ad07608f4")]

If you look in the registry, you’ll see this:

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Interface\{535C044D-EA08-4DC9-8E90-8520B47B65BC}]

@="IMyInterface"

[HKEY_CLASSES_ROOT\Interface\{535C044D-EA08-4DC9-8E90-8520B47B65BC}\ProxyStubClsid32]

@="{00020424-0000-0000-C000-000000000046}"

[HKEY_CLASSES_ROOT\Interface\{535C044D-EA08-4DC9-8E90-8520B47B65BC}\TypeLib]

@="{A8550677-2F41-4071-8363-B05AD07608F4}"

"Version"="1.0"

HKEY_CLASSES_ROOT is more secure and thus requires special permission to write, thus the requirement to run as Admin.

Let’s make our “Class1” implement the interface IMyInterface. Type “ : IMyInterface after “Class1”. You’ll see something like this:

clip_image002

Click on that underline thingy (or use Ctrl-.) to invoke the Smart Tag Action to automatically implement the interface, which inserts this code:

public string GetAStringFromParameters(string strParam, int intParam)

        {

throw new NotImplementedException();

        }

Pretty cool?

Replace the Not Implemented line with our fancy implementation:

return strParam + intParam.ToString();

Now let’s change the method “ManagedMethodCalledFromExtension” to return the interface implementer: the instance of “Class1”

Because “Class1” is now a COM object, it needs to be registered too, so add a ComVisible and GUID to it too.

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{E1CB52AD-A87E-4679-84DA-14E759FB51E4}]

@="ClassLibrary1.Class1"

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{E1CB52AD-A87E-4679-84DA-14E759FB51E4}\Implemented Categories]

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{E1CB52AD-A87E-4679-84DA-14E759FB51E4}\Implemented Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}]

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{E1CB52AD-A87E-4679-84DA-14E759FB51E4}\InprocServer32]

@="mscoree.dll"

"ThreadingModel"="Both"

"Class"="ClassLibrary1.Class1"

"Assembly"="ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"

"RuntimeVersion"="v4.0.30319"

"CodeBase"="file:///C:/Users/calvinh/documents/visual studio 2013/Projects/StartClr/Debug/ClassLibrary1.dll"

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{E1CB52AD-A87E-4679-84DA-14E759FB51E4}\InprocServer32\1.0.0.0]

"Class"="ClassLibrary1.Class1"

"Assembly"="ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"

"RuntimeVersion"="v4.0.30319"

"CodeBase"="file:///C:/Users/calvinh/documents/visual studio 2013/Projects/StartClr/Debug/ClassLibrary1.dll"

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{E1CB52AD-A87E-4679-84DA-14E759FB51E4}\ProgId]

@="ClassLibrary1.Class1"

Now when you single step through the C++ program, you get some number back: in my case: dwResult == 0x04540030

The C++ program now needs to use the TypeLibrary generated by the C# program to call the code (and display intellisense too!)

C++ can “import” a type lib:

#if _DEBUG

#import "..\Debug\ClassLibrary1.tlb" no_namespace

#else

#import "..\Release\ClassLibrary1.tlb" no_namespace

#endif

One reason to have a different #import for debug and release builds is because the paths are different.

The #Import generates some C++ files: examine ClassLibrary1.tlh and ClassLibrary1.tli

clip_image003

Now we’ll take the return value (dwResult) from calling the static C# method “ManagedMethodCalledFromExtension” and interpret it as a pointer to an IUnknown interface.

Then we’ll do a QueryInterface on that IUnknown for the IMyInterface interface, then call a method on the interface.

CComQIPtr<IMyInterface> pIMyInterface = (LPUNKNOWN) dwResult;

auto result = pIMyInterface->GetAStringFromParameters(L"Are you surprised that this works? ", 10);

You’ll notice that there are some methods on the interface that we didn’t explicitly add, like “GetTypeInfo”. These are members of the IDispatch interface. If you don’t want these methods, make your interface  inherit from IUnknown directly:

    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]

You can expand on this idea by adding more methods, more interfaces, and even implementing some interfaces in C++.

Reg-Free COM: Because the C++ program knows about the C# library beforehand (it knows the name of a type and the name of an interface that that type implements), it doesn’t need to rely on registered COM interfaces, so no registration is necessary.

So you see that the ability to call a static method that takes a single string parameter and returns an integer can be quite useful!

See also

Strongly typed methods and properties

Create an ActiveX control using ATL that you can use from Fox, Excel, VB6, VB.Net

A Visual Basic COM object is simple to create, call and debug from Excel

Write Fox code in Visual Studio that interacts with your VB.Net code

<C++ Code>

 // StartClr.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <metahost.h>
#include <atlbase.h>
#include <atlcom.h>
#if _DEBUG
#import "..\Debug\ClassLibrary1.tlb" no_namespace
#else
#import "..\Release\ClassLibrary1.tlb" no_namespace
#endif


#define IfFailRet(expr)                { hr = (expr); if(FAILED(hr)) return (hr); }
#define IfNullFail(expr)                { if (!expr) return (E_FAIL); }

extern "C" int __declspec(dllexport) CALLBACK CallClrMethod(
    const WCHAR *AssemblyName,
    const WCHAR *TypeName,
    const WCHAR *MethodName,
    const WCHAR *args,
    LPDWORD pdwResult
    )
{
    int hr = S_OK;
    CComPtr<ICLRMetaHost> spHost;
    hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&spHost));
    CComPtr<ICLRRuntimeInfo> spRuntimeInfo;
    CComPtr<IEnumUnknown> pRunTimes;
    IfFailRet(spHost->EnumerateInstalledRuntimes(&pRunTimes));
    CComPtr<IUnknown> pUnkRuntime;
    while (S_OK == pRunTimes->Next(1, &pUnkRuntime, 0))
    {
        CComQIPtr<ICLRRuntimeInfo> pp(pUnkRuntime);
        if (pUnkRuntime != nullptr)
        {
            spRuntimeInfo = pp;
            break;
        }
    }
    IfNullFail(spRuntimeInfo);

    BOOL bStarted;
    DWORD dwStartupFlags;
    hr = spRuntimeInfo->IsStarted(&bStarted, &dwStartupFlags);
    if (hr != S_OK) // sometimes 0x80004001  not implemented  
    {
        spRuntimeInfo = nullptr;
        IfFailRet(spHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&spRuntimeInfo)));
        bStarted = false;
    }

    CComPtr<ICLRRuntimeHost> spRuntimeHost;
    IfFailRet(spRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&spRuntimeHost)));
    if (!bStarted)
    {
        hr = spRuntimeHost->Start();
    }
    hr = spRuntimeHost->ExecuteInDefaultAppDomain(
        AssemblyName,
        TypeName,
        MethodName,
        args,
        pdwResult);
    return hr;
}


int _tmain(int argc, _TCHAR* argv[])
{
    DWORD dwResult;
    HRESULT hr = CallClrMethod(
        L"ClassLibrary1.dll",  // name of DLL (can be fullpath)
        L"ClassLibrary1.Class1",  // name of managed type
        L"ManagedMethodCalledFromExtension", // name of static method
        L"some args",
        &dwResult);

    CComQIPtr<IMyInterface> pIMyInterface = (LPUNKNOWN) dwResult;
    auto result = pIMyInterface->GetAStringFromParameters(
        L"Are you surprised that this works? ", 
        10);
    return 0;
}

</C++ Code>

<C# Code>

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace ClassLibrary1
{
    [ComVisible(true)]
    [Guid("E1CB52AD-A87E-4679-84DA-14E759FB51E4")]
    public class Class1 : IMyInterface
    {
        /// <summary>
        /// a managed static method that gets called from native code
        /// </summary>
        public static int ManagedMethodCalledFromExtension(string args)
        {
            var retval = 0;
            try
            {
                // now we want to create an instance of a class that implements IMyInterface.
                var anInstance = new Class1();
                // however, since this is a typical managed object, the garbage collector
                // might move the object around in memory, so we need to create a wrapper for it
                // that won't move around
                var ptr = Marshal.GetComInterfaceForObject(anInstance, typeof(IMyInterface));
                retval = ptr.ToInt32();
            }
            catch (Exception ex)
            {
                retval = Marshal.GetHRForException(ex);
            }

            return retval;
        }

        public string GetAStringFromParameters(string strParam, int intParam)
        {
            return strParam + intParam.ToString();
        }
    }

    [ComVisible(true)]
    [Guid("535C044D-EA08-4DC9-8E90-8520B47B65BC")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IMyInterface
    {
        string GetAStringFromParameters(string strParam, int intParam);
    }
    public interface ILamp
    {
        bool IsOn { get; set; }
        void TurnOn();
        void TurnOff();
    }
}

</C# Code>