Search code examples
c#pinvokemarshalling

Reverse Pinvoke: passing array length without an explicit function parameter for size


I am writing a C# API to a C++ library that takes a function pointer with this signature:

typedef int MyCallback(
  int n,
  int m,
  const double * x, 
  const double * l,
  double * c);

Array x has size n, array c has size m, and array l has size m + n.

I cannot change the signature of the C++ MyCallback function.

The question:

I've passed the arrays from C# using MarshalAs(UnmanagedType.LPArray, SizeParamIndex = <approriate_index>). this is fine for x and c. How do I marshal l and keep track of its size, when I don't explicitly have a parameter with its size?

The only solution I know of that works is to pass IntPtr from C# and use unsafe and ToPointer(). Is there a another solution?

An example of what I'm trying to do is below:

C# code:

using System.Runtime.InteropServices;

namespace PInvokeTest
{
    public class Program
    {
        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        private delegate double MyCallback(
            int n,
            int m,
            [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] double[] x,
            [In, MarshalAs(UnmanagedType.LPArray)] double[] l,
            [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] double[] c);

        private static double CallbackFunction(
            int n,
            int m,
            [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] double[] x,
            [In, MarshalAs(UnmanagedType.LPArray)] double[] l,
            [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] double[] c)
        {
            // THIS IS WILL NOT WORK, l HAS SIZE 1.
            // In this example, only l[0] and l[1] are used, but in general
            // the size of l is n + m.
            c[0] = x[0] + x[1] + x[2] + l[0]*l[1];
            c[1] = x[0] * x[1] * x[2];
            return c[0] + c[1];
        }

        private static MyCallback _myCallback;

        [DllImport("NativeLib", CallingConvention = CallingConvention.StdCall)]
        private static extern int f(MyCallback cf);

        private static void Main()
        {
            _myCallback = CallbackFunction;
            f(_myCallback);
        }
    }
}

Header file:

#ifndef _NATIVELIB_H_
#define _NATIVELIB_H_

#ifndef MYAPI
  #define MYAPI 
#endif

#ifdef __cplusplus
extern "C"
{
#endif

  typedef int MyCallback(
    int n,
    int m,
    const double * x, 
    const double * l,
    double * c);

  MYAPI int f(MyCallback * fnPtr);

#ifdef __cplusplus
}
#endif

#endif // _NATIVELIB_H_

C++ source:

#include "NativeLib.h"
#include <stdio.h>
#include <malloc.h>

MYAPI int f(MyCallback * fnPtr)
{
  int n = 3;
  int m = 2;

  double x[] = { 1.0, 2.0, 3.0 };
  double l[] = { 1.0, 1.0, 1.0, 1.0, 1.0 };
  double c[] = { 0.0, 0.0};

  printf("%e\n", fnPtr(n, m, x, l, c));

  printf("the value of c after the function call:\n");

  printf("%e %e\n", c[0], c[1]);
  return 0;
}

Solution

  • You'd need a custom marshaller for this callback. But there is little point, you can simply marshal the array yourself:

    private static double CallbackFunction(..., IntPtr lptr, ...) {
        var l = new double[n + m];
        Marshal.Copy(lptr, l, 0, l.Length);
        // etc...
    }
    

    Update the delegate signature to match.

    Do note that all the arrays are getting copied. That is pretty expensive, you may well favor using pointers so no copying is required:

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    private unsafe delegate double MyCallback(int n, int m, double* x, double* y, double* c);
    

    Update CallbackFunction() to match and use Project + Properties, Build tab, tick "Allow unsafe code". This tends to invoke the Eek! response, there just aren't that many ways to fumble the code and write beyond the array boundaries if your snippet is a decent match for the real code. Pick good names for these arguments to avoid the other eek.