Jump to content
  • Advertisement
Sign in to follow this  
  • entries
    17
  • comments
    4
  • views
    32481

Lets talk about COM, InterOp, and Manifests

Sign in to follow this  
Mathucub

1986 views

Manifests are loaded two ways: by the OS loader, and manually. They can
either be embedded or the can be on the file system. For MFC Dll's MS
decided that it would manually be managed by the MFC state source. (See my
entry from last week if you missed it.)

It can be argued MFC and COM are legacy, but they aren't going away any time
soon. Back when .NET was just a baby and COM was the ruler MS had to define
a way for the two problem domains to talk to each other so that .NET could
be adopted.

Just as .NET has revolutionized the world of coding by having multiple
languages talk to a common runtime; thus eliminating the short comings of
specific languages and providing a common type system to all assemblies. Back
in the day, COM did the same thing by allowing types to be shared by dll's and applications.

It didn't fix the issue that some languages simply lacked functionality
(such as VB not having true OO and having to reach backwards over your shoulder
to access win32 calls) it did make it so that as long as your compiler
produced COM compatible types, you could mix and match any language at runtime
by communicating through the COM interfaces of either IUnknown or IDispatch.

[When using C++ COM we typically write in an intermediate language called IDL.
COM is so old that when IDL is compiled: it will give you a header that
contains C++ classes and raw old school c with manual implementation of
vtables for you interfaces.]

When a COM project is compiled it will produce something called a type library.
These are "tlb" files that contain the prototypes for all interfaces, structs,
enumerations, etc that the COM library supports. Each of these items should
have an associated globally unique identifier (GUID). GUID's are 128bit numbers
that are most likely unique in the universe. When a COM library is
registered these GUIDs are placed into the registry so that other COM
libraries or hosting containers can find them.

The whole point of Reg Free COM get around this by placing all the registry
information into a manifest file. Otherwise when version 2.0 of GUID xyz is
placed in the registry, it would overwrite version 1.0 of GUID xyz -- since
they have the same unique fingerprint.

If each time you release a new version of your COM library you changed your
GUIDs then there would be no competition and you could install multiple
versions; however, when you instantiate a COM interface you call
CoCreateInstance(GUID, ...). If you change your GUIDs with every release, old
client code will not be able to talk to your new versions.

It can be said that ActiveX controls are COM controls that have a UI element.
(I'm glossing over a lot by saying that but it makes this discussion stay
on scope.) Most people will have encountered ActiveX controls as IE plugins.
Windows UI common controls and well as UI controls for visual basic 6.0 are in
this same family of ActiveX conrols. [Although they are not necessarily
ActiveX, they work the exact same way.] These controls are no good on their own
and must be hosted in an application. What good would a button control be if it
were not on the UI to click?

Taking all this into account, for .NET to work with COM it must:
1) Understand COM variable types
2) Instance and call COM Interfaces
3) Be able to host an ActiveX control

Note: for case 1 the InterOp layer must be convert types. Eg, COM uses
a type of string called a BASIC String (BSTR) that is like a pascal string
that is allocated by ::SysAllocString. .NET uses System::String, InterOp
will do something called "Marshalling" to make one work with the other.
In COM there is a catch all type called a VARIANT that will hold strings,
numbers, interfaces, almost anything. Marshaling a VARIANT can be tricky.

This is where you need to specifically understand how an application or control
can run. I have already talked about executing a standard program: the manifest
file will be loaded and used for the entire app. If your COM is either Reg Free
or requires a specific runtime then it will need its own manifest. As I said
earlier it is the dll's responsibility to manage its own manifest thus managing
its own Activation Context (ActCtx).

How does the relate to the COM InterOp requirements above? Well, we need
to look at how the 3 requirements are satisfied. The first is with an interop
assembly. [I call these tl-InterOp due to the way they are constructed.]
To create one, compile your COM dll and run this:


tlbimp.exe myLibrary.dll


This will produce a new dll that has all your COM types. If you add a reference
in your project to it all the types will magically appear. Lets disassemble
this file and see what is inside it:


namespace VTXComputeLib
{
[ComImport, Guid("11D77BD1-500B-477E-91FE-724116A7F9DE"), TypeLibType((short) 0x10c0)]
public interface IVxPanorama

[ComImport, CoClass(typeof(VxPanoramaClass)), Guid("11D77BD1-500B-477E-91FE-724116A7F9DE")]
public interface VxPanorama : IVxPanorama

[ComImport, ClassInterface((short) 0), Guid("10BA6F3D-BB38-4264-BE4D-62A5F5A2D059"), TypeLibType((short) 2)]
public class VxPanoramaClass : IVxPanorama, VxPanorama
}



Okay, pretty clear what is going on here. This is just a list of the interfaces
in my COM library with their GUIDS and type info. So this dll simply maps the type GUIDs in the registry to my file.

The second assembly is the requirement that .NET be able to call COM code.
We have already partially gotten this requirement from the tlbimp step. It will
give us the ability to make the calls; however, .NET is a garbage collected
language. COM is reference counted. This can cause major problems because .NET
won't necessarily tear COM components down in the correct order when they are
done. This can be done by adding "using System.Runtime.InteropServices;" to your .NET code.

There is a lot of power in the InteropServices. You can hand .NET interfaces
back to COM, you can do manual interface releases, etc. Look at the namespace
on msdn. I can't go into all the power of this guy.

Now for the final requirement (and the reason we have to break out the custom
code). Our .NET UI application has to be able to host our ActiveX control.
Remember that we are running without our control being registered. When
are the two times this happens? 1) At runtime--we can handle this by adding a
simply adding the reference to our library into our application's manifest.










Here is the issue--how do you add controls to a form? In visual studio!
For our application to find our control we modified its manifest. That
obviously is not going to work for visual studio. If this were an MFC dll, then
then we would already be done. We would just embed the manifest above as a
RT_MANIFEST and MFC would switch contexts on its own.

We used tlbimp to get our tl-InterOp assembly. To get an ActiveX one
[ax-InterOp] we use aximp.exe. If you are working on a project that will run
with its components registered: you only need to call "aximp.exe myLib.dll."

It will give you a precompiled assembly with your fittings into .NET.
Luckily, aximp has a command line option "/source" that will give you c# code.
Make a new project and add the source to it. Aximp will give you something that
looks like:

namespace AxMyControlLib
{
public class AxMyScene : System.Windows.Forms.AxHost
...

public AxMyScene() :
base("77dbba22-ecfc-4c1f-90d2-b081a61c5ebd")
{

}

protected override object CreateInstanceCore(Guid clsid)
{
RetObject = base.CreateInstanceCore(clsid);
}

// ...
}


First things first, aximp defaults to giving you code that you cannot add
to visual studio's toolbox palette. You need this if your clients are going
to start a new project. [If your library were registered they could add
it to the toolbox by browsing to it, now they need to browse to your
axInterop dll.]

change:

public class AxMyScene : System.Windows.Forms.AxHost


into:

[System.ComponentModel.DesignTimeVisibleAttribute(true)]
[System.ComponentModel.ToolboxItemAttribute(true)]
public class AxMyScene : System.Windows.Forms.AxHost


Now your clients will love you because they can start new projects!

Looking at the ax-Interop source, you will notice that there is no ActCtx code!
MS made it a convention that dll's should self manage if they have
embedded manifests. This is not a free process though: It looks like the guys
that wrote the AxImp tool didn't get the memo from the VC++ team.

The next step is converting the MFC code I listed last week into C#.


public class CActivationContextState
{
////////////////////////////////////////////////////////////
// PInvoke
[DllImport("Kernel32.dll", SetLastError = true)]
private extern static IntPtr CreateActCtx(ref ACTCTX actctx);

[DllImport("Kernel32.dll", SetLastError = true)]
private extern static bool ActivateActCtx(IntPtr hActCtx, out uint lpCookie);

[DllImport("Kernel32.dll", SetLastError = true)]
private extern static bool DeactivateActCtx(uint dwFlags, uint lpCookie);

[DllImport("Kernel32.dll", SetLastError = true)]
private extern static bool ReleaseActCtx(IntPtr hActCtx);

[DllImport("kernel32.dll")]
static extern uint FormatMessage(uint dwFlags, IntPtr lpSource,
uint dwMessageId, uint dwLanguageId, [Out] StringBuilder lpBuffer,
uint nSize, IntPtr Arguments);

[DllImport("Kernel32.dll", SetLastError = true)]
static extern uint FormatMessage(uint dwFlags, IntPtr lpSource,
uint dwMessageId, uint dwLanguageId, ref IntPtr lpBuffer,
uint nSize, IntPtr pArguments);

[DllImport("Kernel32.dll", SetLastError = true)]
static extern uint FormatMessage(uint dwFlags, IntPtr lpSource,
uint dwMessageId, uint dwLanguageId, ref IntPtr lpBuffer,
uint nSize, string[] Arguments);

[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr LocalFree(IntPtr hMem);

[StructLayout(LayoutKind.Sequential)]
private struct ACTCTX
{
public int cbSize;
public uint dwFlags;
public string lpSource;
public ushort wProcessorArchitecture;
public ushort wLangId;
public string lpAssemblyDirectory;
public UInt16 lpResourceName;
public string lpApplicationName;
}

private const uint ACTCTX_FLAG_PROCESSOR_ARCHITECTURE_VALID = 0x001;
private const uint ACTCTX_FLAG_LANGID_VALID = 0x002;
private const uint ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID = 0x004;
private const uint ACTCTX_FLAG_RESOURCE_NAME_VALID = 0x008;
private const uint ACTCTX_FLAG_SET_PROCESS_DEFAULT = 0x010;
private const uint ACTCTX_FLAG_APPLICATION_NAME_VALID = 0x020;
private const uint ACTCTX_FLAG_HMODULE_VALID = 0x080;

private const UInt16 RT_MANIFEST = 24;
private const UInt16 CREATEPROCESS_MANIFEST_RESOURCE_ID = 1;
private const UInt16 ISOLATIONAWARE_MANIFEST_RESOURCE_ID = 2;
private const UInt16 ISOLATIONAWARE_NOSTATICIMPORT_MANIFEST_RESOURCE_ID = 3;

private const uint FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100;
private const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
private const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;
////////////////////////////////////////////////////////////


private uint cookie = 0;
private static ACTCTX actCtx;
private static IntPtr hActCtx = IntPtr.Zero;
private static bool contextCreationSucceeded = false;

public bool EnterActCtx()
{
if (!contextCreationSucceeded || cookie != 0)
return false;

return ActivateActCtx(hActCtx, out cookie);
}

public bool ExitActCtx()
{
if (cookie == 0)
return false;

bool bRet = false;

try
{
bRet = DeactivateActCtx(0, cookie);
cookie = 0;
}
catch (Exception Ex)
{
System.Diagnostics.Debug.Print(Ex.ToString());
PrintError(Marshal.GetLastWin32Error());
}

return bRet;
}

public CActivationContextState()
{
if (EnsureActivateContextCreated())
{
if (!EnterActCtx())
{
// Be sure cookie always zero if activation failed
cookie = 0;
}
}
}

~CActivationContextState()
{
ExitActCtx();

if (contextCreationSucceeded)
{
ReleaseActCtx(hActCtx);
}
}

private void PrintError(int Error)
{
IntPtr lpMsgBuf = IntPtr.Zero;

uint dwChars = FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
IntPtr.Zero,
(uint)Error,
0, // Default language
ref lpMsgBuf,
0,
IntPtr.Zero);

if (dwChars == 0)
{
// Handle the error.
int le = Marshal.GetLastWin32Error();
}

string sRet = Marshal.PtrToStringAnsi(lpMsgBuf);

// Free the buffer.
lpMsgBuf = LocalFree(lpMsgBuf);

System.Diagnostics.Debug.Print(sRet);
}

private bool EnsureActivateContextCreated()
{
try
{
string rgchFullModulePath = null;
rgchFullModulePath = System.Reflection.Assembly.GetExecutingAssembly().Location;

actCtx = new ACTCTX();
actCtx.cbSize = Marshal.SizeOf(typeof(ACTCTX));
actCtx.dwFlags = ACTCTX_FLAG_RESOURCE_NAME_VALID;
actCtx.lpSource = rgchFullModulePath;
actCtx.lpResourceName = ISOLATIONAWARE_MANIFEST_RESOURCE_ID;

hActCtx = CreateActCtx(ref actCtx);
contextCreationSucceeded = (hActCtx != new IntPtr(-1));

if (!contextCreationSucceeded)
{
PrintError(Marshal.GetLastWin32Error());

actCtx.lpResourceName = ISOLATIONAWARE_NOSTATICIMPORT_MANIFEST_RESOURCE_ID;
hActCtx = CreateActCtx(ref actCtx);
contextCreationSucceeded = (hActCtx != new IntPtr(-1));
}
if (!contextCreationSucceeded)
{
PrintError(Marshal.GetLastWin32Error());

actCtx.lpResourceName = CREATEPROCESS_MANIFEST_RESOURCE_ID;
hActCtx = CreateActCtx(ref actCtx);
contextCreationSucceeded = (hActCtx != new IntPtr(-1));
}
}
catch (Exception Ex)
{
System.Diagnostics.Debug.Print(Ex.ToString());
}

return contextCreationSucceeded;
}
};


C# is restrictive about when it lets you call your base class's constructor.
It has to be declared with your class's constructor and called right before it.
That is problematic for us because we need to switch to our Reg Free context
before our base class is called.

Luckily, initializers run before constructors, so when the class is created
it will automatically hop to the correct context.

Now your code should look like this:

[System.ComponentModel.DesignTimeVisibleAttribute(true)]
[System.ComponentModel.ToolboxItemAttribute(true)]
public class AxMyScene : System.Windows.Forms.AxHost
{
// this will enter the actctx
private CActivationContextState ActCtxState = new CActivationContextState();

private System.Windows.Forms.AxHost.ConnectionPointCookie cookie;

public AxMyScene() :
base("77dbba22-ecfc-4c1f-90d2-b081a61c5ebd")
{
ActCtxState.ExitActCtx();
}

protected override object CreateInstanceCore(Guid clsid)
{
Object RetObject = null;

try
{
ActCtxState.EnterActCtx();
RetObject = base.CreateInstanceCore(clsid);
}
catch (Exception Ex1)
{

}
finally
{
try
{
ActCtxState.ExitActCtx();
}
catch (Exception Ex)
{

}
}

return RetObject;
}

// ...
}


So, when we are created, we load our context and switch to it. That makes us
hit our library without being registered. When someone tries to create us, we
switch contexts to our registration free one. Remember, you must always push
and pop your ActCtx or you will pollute the calling application. This is why
the constructor calls exit.

Depending on how you've coded your ActiveX control, you may have
to add the Enter/Exit code to more methods. Try your control out in visual
studio and you will quickly get exceptions if something is wrong.

To debug this, you will need to start two copies of visual studio.
Attach the first's debugger to the second. In the second add your
control to your toolbox, and from the toolbox to the form.
You should be good to go!

Now you can have as many versions as you want and let them fully interoperate
with managed code.

As always, pvt me if you have any specific questions.
Sign in to follow this  


0 Comments


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

Participate in the game development conversation and more when you create an account on GameDev.net!

Sign me up!