The Pinvoke Diary: How to send a triple pointer to a native world from c#

We are back again with a new pinvoke scenario. In the recent days I had the opportunity to work with one of our clients who was trying to utilize a native library in his .net application and was failing to do so, as his application was crashing due to marshaling issues. As you have seen from earlier posts when dealing with issues where you will have to send data types to the native world marshaling plays an important role . In this particular scenario we had a rather complex request of sending the triple pointer. While there are ways you can marshal the data to the native , much easier way to do this would be to use unsafe context.

                                           We need to compile the .net application with /unsafe. To maintain type safety and security, C# does not support pointer arithmetic, by default. However, by using the unsafe keyword, you can define an unsafe context in which pointers can be used .The unsafe keyword denotes an unsafe context, which is required for any operation involving pointers. You can use the unsafe modifier in the declaration of a type or a member. For more information about pointers, see the topic Pointer types. In the common language runtime (CLR), unsafe code is referred to as unverifiable code. Unsafe code in C# is not necessarily dangerous; it is just code whose safety cannot be verified by the CLR. The CLR will therefore only execute unsafe code if it is in a fully trusted assembly. If you use unsafe code, it is your responsibility to ensure that your code does not introduce security risks or pointer errors. .

We had the function signature like (int) inatrace_initialize(int *argc, char **argv[]);
Here is how the sample looks like ,

The native component had a definition like below , Our client did not have control over it but we created a simple dll so as to call into it from the managed world.
Here is the .h file

 #ifdef TESTDLL_EXPORTS
 #define TESTDLL_API __declspec(dllexport)
 #else
 #define TESTDLL_API __declspec(dllimport)
 #endif
 
 extern "C"
 {
 TESTDLL_API int dynatrace_initialize(int* argc, char**argv[]);
 TESTDLL_API void dynatrace_uninitialize(int* argc, char**argv[]);
 }

This is the .cpp file

 extern "C" TESTDLL_API int dynatrace_initialize(int* argc, char**argv[])
 {
 // heap allocate array of char ptrs of length *argc.
 char** parray = (char**)malloc(*argc * sizeof(char*));
 
 for (int i = 1; i <= *argc; i++)
 {
 char buffer[1024] = { 0 }; // local buffer for sprintf
 sprintf_s(buffer, 1024, "Test String %d", i); // create output string in local buffer
 parray[i-1] = (char*)malloc(strlen(buffer)+1); // allocate heap buffer
 strcpy_s(parray[i-1], strlen(buffer)+1, buffer); // copy local var buffer to heap buffer
 }
 
 *argv = parray;
 return *argc;
 }

One thing to note – We have wrapped our C++ code in ‘extern “C”’ declarations to turn off C++ name-decoration. If your native code does not do this, you’ll need to use the C++ decorated name in the EntryPoint attribute here. This can be found the ‘dumpbin.exe /exports’ run on the DLL name.

Now coming to the main part , the usage of unsafe code ,

 

 //Setting the dllimport 
 
 [DllImportAttribute(@"D:\Sample\Debug\TestDll.dll", EntryPoint = "inatrace_initialize", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] 
 public unsafe static extern int inatrace_initialize(int* argc, char*** argv);
 
 
 //Here is the main
 
 namespace ManagedConsole
 {
 class Program
 {
 static void Main(string[] args)
 {
 string[] results;
 results = Program.GetUnsafeResults(11);
 if (results != null)
 {
 Console.WriteLine("Found {0} results:", results.Length);
 foreach (var item in results)
 {
 Console.WriteLine(item);
 }
 }
 }
 }
 
 static string[] GetUnsafeResults(int count)
 {
 string[] results = null;
 unsafe
 {
 int argc = count;
 char** argv;
 
 count = dynatrace_initialize(&argc, &argv);
 
 results = new string[argc];
 for (int i = 0; i < count; i++)
 {
 results[i] = Marshal.PtrToStringAnsi(new IntPtr(argv[i]));
 }
 
 // Uninitialize the dynamically generated memory
 inatrace_uninitialize(&argc, &argv);
 }
 return results;
 }
  

There are scenarios when you would not want the memory to be garbage collected, fixed statement can be used to declare pointers . It helps in pinning the location of the objects in memory so that they will not be moved by garbage collection until the execution of fixed block . One more advantage is the performance improves with the usage of unsafe block. Thanks to my escalation engineer Steve Horne for helping us out on the scenario.

 

Keep Coding!
Nandeesh Swami