Have you come across the need for code interaction in C # and native-C ++ (or rather C)? The reasons could be different: the library is already there, it is easier to write in C / C ++, the development of parts of the application is carried out by different teams, _______________ (you need to enter).
It is known that languages ​​are based on completely different sets of axioms.
In C # (CLR, more precisely) you are dealing with types of fixed sizes (with rare reservations), the code can be compiled by the JIT compiler for any of the supported target platforms (unless explicitly stated otherwise).
')
In the C ++ world, everything is completely different: the same types can have different sizes when compiled to different platforms (hello, size_t), the code is generated differently for different platforms, operating systems and other delights.
Under the cut, we will try to make friends with these features.
For interacting managed (managed) with unmanaged (native, unmanaged) code, in which unmanaged libraries are connected to a managed application, there is a Platform Invoke (p / Invoke) mechanism. This interaction is classified as in-process.
It has the following limitations:
- It is possible to call only unmanaged functions, but you cannot access exported variables;
- Imported functions become static class methods;
- Imported functions are declared as extern and are marked with a special attribute DllImport, which indicates to the compiler that it is necessary to generate a special call marshaling code;
- In the process of calling unmanaged code, the thread that executes it cannot be interrupted, unlike the C # code. So, if Abort or Interrupt is called on it, then raising the exceptions will be postponed until it returns to a managed context;
The list, of course, is incomplete, but gives an idea of ​​what is happening.
We will not consider all aspects of working with p / Invoke, but will focus only on how for p / Invoke to solve the challenge on different architectures (using x86 and x64 as an example), and we will not touch on other architectures and operating systems, however, what will be described in the article is theoretically enough to develop a thought further. We will consider this as homework for those who need it.
So let's unwind the ball.
We need to import some set of functions from unmanaged libraries in C ++ to call them from C # code, while supporting two architectures at the same time: x86 and x64, choosing them depending on which platform the C # host application is running on.
I use MS Visual Studio 2015 Community Edition for an example, but everything should work when developing using other tools. CMake and other delights (so far) do not bother.
The source code for the evolution process is available
on the githab by reference .
After creating a solution with two projects (CrossPlatformInterop of Console Application type in C # and CrossPlatformLibrary of Win32 Project / DLL type), we configure them so that the output directory is $ (SolutionDir) Output \ $ (Configuration) \, and for C ++ project the name of the file being collected is $ (ProjectName) - $ (PlatformShortName) .dll in order to get different files on x86 and x64.
Configuration results can be viewed in the project-setup branch in the repository.
We implement a simple C ++ function that takes 2 numbers and simulates a vigorous activity in the form of formatting a string and passing it to a managed code via a callback function:
Note that the sizes of the data types and the call convention are explicitly indicated here. Since we interact with another language, we have to know this, and the rules for writing portable C code in C ++ do not work here. But, unlike types like size_t, we always know what type of fixed-size C # it corresponds to.
Here there is one subtlety: the pointer, which in C ++ looks like void * or T *, has a different size for different platforms, but at the same time from the C # side it is translated into a special type IntPtr, which also has a variable size. So the compiler itself helps us with marshalling pointers.
When the compiler operates with names, it converts them, encoding in them the types of objects, arguments, return values, call conventions, and much more. This operation is called decoration (mangling). So, the name of the function by the Microsoft compiler is converted to the form? ProcessData @@ YGHHHP6GXPBD @ Z @ Z or? ProcessData @@ YAHHP6AXPEBD @ Z @ Z (find one difference - it depends on the pointer size). You saw something like this, when the linker in C ++ projects swore?
It is inconvenient to work with such names, so we will ask the compiler in the external software interface to bring them to a more readable form by adding
extern "C"
to the declaration. If you use the convention of calling __ cdecl, then there are no questions, but if you use __stdcall, then the name will still not become “normal”, but will look like _ProcessData @ 12 for x86 (after the dog the number of bytes on the stack is indicated). You can, of course, make a def-file in the project and specify a list of functions for export there, but we will not do that.
We will work with __stdcall, because in Windows it is customary to use this convention when working with libraries.
Further, in order to import this function, it would be enough to write the following code:
public class LibraryImport { [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet=CharSet.Ansi)] public delegate void Notification(string value); [DllImport("CrossPlatformLibrary-x86", CallingConvention=CallingConvention.StdCall)] public static extern int ProcessData(int start, int count, Notification notification); }
Usage might look like:
LibraryImport.ProcessData(1, 10, s => Console.WriteLine(s));
But if our code is executed in a 64-bit environment, then when loading a class, the BadImageFormatException exception will be raised, that is, an attempt to load an image of an incompatible format library. I hope, to explain why the images are incompatible, it is not necessary. When importing a 64-bit library from a 32-bit environment, there will be the same problem.
Of course, one could say that we are rapidly completing the second decade of the 21st century, and it’s time to bury 32-bit systems, but I wouldn’t hurry because if I have a tablet on a Windows 32-bit system, and I still have An old iron park at work, where 32-cues also spin. And in general, the approach will be fair and the transition to other processor architectures (will we live to see the happy moment when ARMs and other Baikals will be fully supported in the dotner?).
There is another problem in this code, but we will look at it later.
Now let's do a full import. Take note of the fact that type loading in .NET is lazy, that is, until some class is needed by the runtime, it will not be parsed and compiled. That is, if we do not refer to the type, there may be an import of an incorrect library.
The first thing to do is to hide the imported methods from prying eyes. In general, to give something from the inner kitchen, too sticking out, is bad. Imported methods will be made private, and we will provide wrapper methods to the outside. And the list of methods will make the interface.
Source [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)] public delegate void Notification(string value); public interface ILibraryImport { int ProcessData(int start, int count, Notification notification); } internal class LibraryImport_x86 : ILibraryImport { [DllImport("CrossPlatformLibrary-x86", CallingConvention = CallingConvention.StdCall, ExactSpelling = false, EntryPoint = "_ProcessData@12")] private static extern int ProcessDataInternal(int start, int count, Notification notification); public int ProcessData(int start, int count, Notification notification) { return ProcessDataInternal(start, count, notification); } } internal class LibraryImport_x64 : ILibraryImport { [DllImport("CrossPlatformLibrary-x64", CallingConvention = CallingConvention.StdCall, ExactSpelling = false, EntryPoint = "ProcessData")] private static extern int ProcessDataInternal(int start, int count, Notification notification); public int ProcessData(int start, int count, Notification notification) { return ProcessDataInternal(start, count, notification); } }
Please note that the classes are declared as internal, and the interface is public. In vain I, of course, did not make the wrapper library separate from the application, but oh well: the idea should be clear.
Next you need to make the loader, which will give an instance of the desired class depending on the bitness of the current runtime environment.
Source public static class LibraryImport { public static ILibraryImport Select() { if (IntPtr.Size == 4)
Here it is assumed that we have a choice of only two options. In general, it is here that the decision is made about which classes to use for which implementation. And here you can do a lot of other strange things.
The use is already quite simple:
class Program { static void Main(string[] args) { ILibraryImport import = LibraryImport.Select(); import.ProcessData(1, 10, s => Console.WriteLine(s)); } }
The proposed solution has a big drawback: a sufficiently large amount of duplicate stereotype code: the signature of each imported function is present in five places, and is used in three different ways. And a couple of places in the native code. Unfortunately, I didn’t think of any way to minimize the number of variants, except for code generation. But if you can do something more elegant, I will definitely write.
I promised to point out another problem with this code. It does not relate directly to the topic under discussion, but I still want to mention it. But under the spoiler.
<spoiler title = “Problem of dangling pointers> Suppose that we have an asynchronous call, for example, when we press a button, we will have some code in the background that generates a work protocol and something else that is useful, but the button handler has completed its work, and therefore, all local objects can be collected by the garbage collector. And we have such an object and a very important one: the delegate encapsulating the callback function. After an arbitrary period of time, the code will simply fall with an incomprehensible error in accessing either a null pointer or, even worse, an arbitrary area of ​​memory. And all because the pointer to the function in the unmanaged code is still alive, and the delegate to which it refers is already gone, most likely its memory has been cleared and now we have a dangling pointer.
To prevent this from happening, you need to increase the lifetime of the delegate, either by saving it as a field in the object, or in some other way, depending on the circumstances.
And in this case, you should think about how to stop the unmanaged stream.
I have everything today. I hope someone will be useful.