Renesis
Renesis

Reputation: 899

Correct return value with PInvoke char** (C -> C#)

My problem is that I'm using the MATLAB API from C# and this is the function that is giving me trouble.

C code:

EXTERN_C char ** matGetDir(MATFile * pMF, int *num); 

I expected this to work, but unfortunately it doesn't (C# code):

[DllImport("libmat.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern string[] matGetDir(IntPtr matFile, ref int num); 

If I put IntPtr instead of string[] I can call the function, but then I don't know how to convert the code from IntPtr to string[]

Edit1:
I also tried using these attributes, but it fails with error: Cannot marshal 'return value': Invalid managed/unmanaged type combination.

[return: MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr)]
private static extern string[] matGetDir(IntPtr matFile, ref int num);

Upvotes: 1

Views: 485

Answers (2)

CitizenInsane
CitizenInsane

Reputation: 4855

You can do it this way:

[DllImport("libmx.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void mxFree(IntPtr ptr);

[DllImport("libmat.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]        
private static extern IntPtr matGetDir(IntPtr matFile, ref int num);

public static string[] matGetDir(IntPtr matFile)
{
    // Obtain as IntPtr
    var count = 0;
    var pointers = matGetDir(matFile, ref count);

    // Handling errors as noticed by David Heffernan
    if (count < 0) { throw new Exception("Failed to obtain list of variables in selected mat file."); }
    if (pointers == IntPtr.Zero) { return new string[0]; }

    // Cast into IntPtr[]
    var ptrs = new IntPtr[count];
    Marshal.Copy(pointers, ptrs, 0, count);

    // Convert each value in IntPtr[] into string
    // NB: using System.Linq;
    var strs = ptrs.Select(x => Marshal.PtrToStringAnsi(x)).ToArray();

    // Don't forget to free memory allocated by Matlab
    // NB: Deleting global pointer only ==> see "edit([matlabroot '/extern/examples/eng_mat/matdgns.c']);" example 
    mxFree(pointers);

    // And voilà
    return strs;
}

So just making conversion into IntPtr in a private method as you initally tried and then adding a public method for conversion to string[]. See comments in the code for more details.

Upvotes: 1

David Heffernan
David Heffernan

Reputation: 613063

You cannot tell the p/invoke marshaller how to marshal this for you. You need to do it manually. One thing that is interesting about your question is that you don't give any details of what this char** really is. It's really important for you to learn that a type does not fully define the semantics of a parameter, or in this case a return value.

We can guess easily enough, but it is better to look this up in the documentation: http://uk.mathworks.com/help/matlab/apiref/matgetdir.html

Arguments

mfp

Pointer to MAT-file information

num

Pointer to the variable containing the number of mxArrays in the MAT-file

Returns

Pointer to an internal array containing pointers to the names of the mxArrays in the MAT-file pointed to by mfp. In C, each name is a NULL-terminated string. The num output argument is the length of the internal array (number of mxArrays in the MAT-file). If num is zero, mfp contains no arrays.

matGetDir returns NULL in C (0 in Fortran). If matGetDir fails, sets num to a negative number.

So, we declare the p/invoke like this:

[DllImport("libmat.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr matGetDir(IntPtr matFile, out int num); 

Call it like this:

int num;
IntPtr matFile = ...;
IntPtr namesPtr = matGetDir(mayFile, out num);
if (names == IntPtr.Zero)
    // handle error
string[] names = new string[num];
for (int i = 0; i < num; i++)
{
    int offset = i * Marshal.SizeOf(typeof(IntPtr));
    names[i] = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(namesPtr, offset));
}

Upvotes: 2

Related Questions