📜 ⬆️ ⬇️

.NET in an unmanaged environment: calling managed code from unmanaged

As you probably remember from my previous article , the interaction of unmanaged and managed code presents a certain problem, even for experienced developers. The reason for this is the need to understand what processes occur when the data crosses the CLR boundary.

Unfortunately, often the problem of establishing interaction arises among those developers who are poorly familiar with the inside technology of COM and the capabilities of .NET for ensuring interaction. This is normal - you can not know everything. Therefore, I will not explain here the whole essence of the problem of marshalling data from unmanaged to managed and back, but I’ll just give you some working recipes that will help you when you need urgently and tomorrow, and you look at the English edition of the book Inside OLE and you understand there is no way to figure this out for the day.

However, for those who are good at it, there is a small bonus at the end of the article - a way to organize out-process COM on .NET. Honestly, I honestly thought that it was impossible to do out-process COM using .NET, but just yesterday it turned out that it could not. In this regard, I probably will not talk about the .NET Pipe RPC architecture - it is quite complex, however, out-process COM easily replaces all the opportunities provided to it.

In general, there are two mechanisms offered by Microsoft - dll export and COM export. Yes, that's right - despite the fact that very few people know about the possibility of exporting functions of a managed-code, it still exists. However, I will not talk about it. The fact is that the export of functions is not supported by .NET at a high level, that is, it is impossible to simply describe a function, hang some [DllExport] attribute on it and enjoy life - you will have to either go into the IL jungle or use third-party solutions. Both solutions cannot be called satisfactory, and therefore I will not dwell on this in detail - this mechanism has no practical use at all.
')
The mechanism of COM export is good because its work is transparent enough for a person who understands COM, but for a .NET developer this is a dark forest. That is, for successful work with this mechanism, it is necessary to understand what COM technology is, how calls are made, how parameters should be transmitted, and so on. However, not everyone has knowledge, and therefore common recipes. Let's try to create an inproc-COM server in C # and access it, for example, from VBA. To do this, we use Excel and its built-in scripting language.

Why did I choose Excel? As experience shows, VBA is quite capricious with respect to memory leaks by the client - in the case of the slightest leakage, Excel will fall either immediately upon completion of the procedure, or by unloading the dll (that is, somewhere in a minute after completion of the procedure). Thus, the tilt of Excel helps us understand that we are doing something wrong.

Let's start. Let's create a new assembly, let's call it, as is customary, TestInprocCOM and make one CoClass there that will implement the interfaces through which we will pull it from VBA.

But for a start, let's define what we need from the interaction. As experience shows, most often required to transfer
  1. lines
  2. arrays
  3. structures
  4. other objects


The crown of our today's work will be the transfer of an array of structures in which the lines lie. This is enough for 99% of real applications. For the remaining 1%, the information will be too specific for Habr's wide audience, and therefore I will not focus on this.

I apologize for the too long introduction. I also have to apologize for too detailed an explanation of many seemingly trivial things - I did not know what the level of those people who would read this article was, and wanted to make it understandable to absolutely everyone.

So let's get started.

Implementation of inproc com.



To begin with, we define the interface that will be responsible for the calls. Comments on what this or that line is for, I will do right in the code so as not to increase the already rather big size of the article.

// , COM
[ComVisible( true )]
// . ,
// VBA, . ,
// C++
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
// GUID . - , .
[ Guid ( "5AC0488E-9CAC-4032-B59A-37B8B277C4EF" )]
public interface ITestInterface
{
// COM- , .
// , .NET
// BSTR.
void Hello([MarshalAs(UnmanagedType.BStr)] ref string name);

// , .
[ return : MarshalAs(UnmanagedType.BStr)]
string Say();
}


* This source code was highlighted with Source Code Highlighter .


As you can see, each parameter has a MarshalAs attribute. It is he who says how to marshall the parameters when crossing the CLR boundary. It is not necessary to set it if the marshalling defaults to what behavior you need. However, the default marshall strings are as LPTStr, and therefore for marshalling strings in the form of BSTR you should always set.

// CoClass
[ComVisible( true )]
[ Guid ( "B06CC21C-BCBB-4dde-8ED3-BFBD9A31AD6E" )]
// ProgId , , GUID
[ProgId( "TestInprocCOM.TestCoClass" )]
// , CoClass- .
// , , COM
// - ,
// , .
[ClassInterface(ClassInterfaceType.None)]
// default interface. VBA, ++ .
[ComDefaultInterface( typeof (ITestInterface))]
public class TestCoClass : ITestInterface
{
public void Hello( ref string name)
{
name = "Hello, " + name;
}

public string Say()
{
return "OK" ;
}

}


* This source code was highlighted with Source Code Highlighter .


The object is created. We compile it, and then register it in the system using the regasm /tlb:testinproccom.tlb testinproccom.dll /codebase.

Everything, we have a registered object, there is a type library for it (tlb-file) and we can proceed to the client. To do this, open Excel, go to the Visual Basic Editor, connect our type library via Tools-> References, and write the following:

Dim a As TestCoClass
Dim s As String

Sub main()
Set a = New TestCoClass
s = "mike"
a.Hello s
MsgBox s
msgbox a.Say()
End Sub


* This source code was highlighted with Source Code Highlighter .


We should get two message boxes - the first “Hello, mike” and the second “OK”. Congratulations, we did everything right.

However, one line will not be full. Let's try to transfer the structure. To do this, we define it.

[ComVisible( true )]
// , ,
// unmanaged.
[StructLayout(LayoutKind.Sequential)]
[ Guid ( "E1AB60D5-F8F5-41fe-BD0D-AE2AC94237DD" )]
public struct MyStruct
{
//
[MarshalAs(UnmanagedType.BStr)]
public string wow;
// . SafeArray,
// , VBA JavaScript. , C++ SafeArray
// , . -
// .
[MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_I4)]
public int [] gig;
// IDispatch. .NET
// object.
[MarshalAs(UnmanagedType.IDispatch)]
public object self;
}


* This source code was highlighted with Source Code Highlighter .


Now, let's expand the interface. Let's add one more function there.

// out ref -
// .
void GetStruct([MarshalAs(UnmanagedType.Struct)] ref MyStruct ms);


* This source code was highlighted with Source Code Highlighter .


And we implement it in our CoClass.

public void GetStruct( ref MyStruct ms)
{
ms.gig = new int [] { 1, 2, 3, 4, 5 };
ms.self = this ;
ms.wow = "wow" ;
}


* This source code was highlighted with Source Code Highlighter .


As you can see, we put the this parameter in self - that is, after calling this function in VBA, we will have in this field a link to our object with which we can work with late binding methods (that is, using IDispatch). Let's check the functionality of the object - we recompile, reregister, open Excel again and write the following code:

Dim a As TestCoClass
Dim s As String
Dim b As MyStruct

Sub main()
Set a = New TestCoClass
s = "mike"
a.Hello s
MsgBox s
b.wow = "baaa"
a.GetStruct b
MsgBox b.gig(3)
MsgBox b.self.Say()
MsgBox b.wow
Set a = Nothing
End Sub


* This source code was highlighted with Source Code Highlighter .


If everything went well, then we have the following message boxes
  1. Hello mike
  2. four
  3. Ok
  4. wow


Pay attention to b.self.Say() . We call the Say function of our object using late binding. This mechanism is ensured by the fact that we set the interface as dual (that is, capable of working through vtable and IDispatch simultaneously). That is why it is better to always set interfaces as dual — this does not require any work, but it provides many advantages.

By the way, indirect evidence that everything is working correctly are the hints of the VBA environment.

And finally, the crown number. Array of structures with strings. Define the structure.

[ComVisible( true )]
[StructLayout(LayoutKind.Sequential)]
[ Guid ( "1D40391F-C9CF-42ed-9C9E-4991B3F22907" )]
public struct MyStruct2
{
[MarshalAs(UnmanagedType.BStr)]
public string param;

public MyStruct2( string par)
: this ()
{
param = par;
}
}


* This source code was highlighted with Source Code Highlighter .


We define a function in the interface

// safearray custom- - .
void SetMyStruct2([MarshalAs(UnmanagedType.SafeArray, SafeArrayUserDefinedSubType = typeof (MyStruct2))] out MyStruct2[] structs);


* This source code was highlighted with Source Code Highlighter .


Simply? And no one said it would be difficult. The only thing that is required when setting marshalling is to represent how the client will work with your parameters.

Now, let's implement the function in CoClass.

public void SetMyStruct2( out MyStruct2[] structs)
{
structs = new MyStruct2[] { new MyStruct2( "bla1" ), new MyStruct2( "bla2" ), new MyStruct2( "bla3" ) };
}

* This source code was highlighted with Source Code Highlighter .


Compiling, registering, and - a test case that tells us that everything went smoothly.

Dim a As TestCoClass
Dim sa() As MyStruct2

Sub main()
Set a = New TestCoClass

a.SetMyStruct2 sa
For i = 0 To 2
MsgBox sa(i).param
Next i

Set a = Nothing
End Sub


* This source code was highlighted with Source Code Highlighter .


And if everything went well, then we should receive the following messages:

  1. bla1
  2. bla2
  3. bla3


Hooray us!

Outproc com.



However, the architecture of inproc COM does not always suit us - for example, if we want to create a singleton. Since the dll is projected in the process memory field, different processes can raise the same class instance, which for each process will be unique and will not know anything about its other instances. This is not always what we need.

According to the COM specifications, the outproc implementation of the server is as follows. When a client calls CoGetClassObject with the appropriate parameters, COM first looks at its internal table of class factories, looking for the CLSID specified by the client. If there is no class factory in the table, then COM accesses the registry and starts the corresponding EXE module. The task of the latter is to register its class factories as soon as possible so that COM can find them. To register an EXE class factory, use the COM CoRegisterClassObject.
function CoRegisterClassObject.
CoRegisterClassObject.

So our task is to do it. To do this, create a new project (console EXE application), transfer the code of our interfaces, structures and classes to it. Now, we define the required registration code in the Main function.

According to COM specifications, outproc servers must support self-registration with the / register key. This is done as follows:

static private void RegisterManagedType(Type type)
{
String strClsId = "{" + Marshal.GenerateGuidForType(type).ToString().ToUpper(CultureInfo.InvariantCulture) + "}" ;

// Create the HKEY_CLASS_ROOT\CLSID key.
using (RegistryKey ClsIdRootKey = Registry.ClassesRoot.CreateSubKey( "CLSID" ))
{
// Create the HKEY_CLASS_ROOT\CLSID\<CLSID> key.
using (RegistryKey ClsIdKey = ClsIdRootKey.CreateSubKey(strClsId))
{
ClsIdKey.SetValue( "" , type.FullName);

// Create the HKEY_CLASS_ROOT\CLSID\<CLSID>\LocalServer32 key.
using (RegistryKey LocalServerKey = ClsIdKey.CreateSubKey( "LocalServer32" ))
{
LocalServerKey.SetValue( "" , ( new Uri ( Assembly .GetExecutingAssembly().CodeBase)).LocalPath);
}
}
}
}


* This source code was highlighted with Source Code Highlighter .


Finally, the Main function.

[MTAThread]
static void Main( string [] args)
{

if ((args.Length == 1) && (args[0] == "register" ))
{
RegisterManagedType( typeof (TestCoClass));

Console .WriteLine( "Server registered, press any key to exit" );

Console .ReadKey();

return ;
}

// ?

}


* This source code was highlighted with Source Code Highlighter .


And now we have a problem - call CoRegisterClassObject to register class factories. It would seem that a simple solution is to use the platform invoke for this function and enjoy life. However, it will not work - Microfost explicitly prohibits p / invoke for the function of registering class factories.

The solution came suddenly from the forums. It turns out that in .NET there is a class with an analogue of this function, which serves specifically for this purpose, and it is called RegistrationServices . It has a RegisterTypeForComClients method that does exactly the same thing that we require from the CoRegisterClassObject. method CoRegisterClassObject.

RegistrationServices rs = new RegistrationServices();

rs.RegisterTypeForComClients(
typeof (TestCoClass),
RegistrationClassContext.RemoteServer | RegistrationClassContext.LocalServer | RegistrationClassContext.InProcessServer,
RegistrationConnectionType.MultipleUse);

Console .WriteLine( "Server started, press any key to exit" );

Console .ReadKey();


* This source code was highlighted with Source Code Highlighter .


We compile, register, test, without forgetting to pre-delete registration inproc COM dll. Hooray us!

Source: https://habr.com/ru/post/58240/


All Articles