Marshalling array of strings to char ** in C#

2020-03-03 06:54发布

问题:

I'm calling a C DLL function and need to supply the following C struct:

typedef struct
{
    char      *mTableId;
    char     **mFieldNames;
    int        mNumFields;
    char      *mFilter;
    char      *mSort;
    int        mOffset;
    int        mMaxRecords;
    char      *mTargetRecordFilter;
    int        mSurroundingRecordsCount;
    int       *mOwnerIds;
    int     mNumOwnerIds;
    gsi_bool   mCacheFlag;
} SAKESearchForRecordsInput;

The problem is with char **mFieldNames; I've tried marshalling automatically like this:

[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPTStr, SizeConst = 9)] public String[] mFieldNames;

This way I get an error in Marshal.SizeOf() - can't compute the correct size. Then I decided to deal with pointers manually. It's in fact just a pointer to the array of C strings. Here's my code which is leading to

System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

So I've screwed up pointers somewhere. The code seems OK to me, where is the bug?

C#:

 [StructLayout(LayoutKind.Sequential)]
 unsafe public class SAKESearchForRecordsInput {
  [MarshalAs(UnmanagedType.LPTStr)]
  public String mTableId;
  //[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPTStr, SizeConst = 9)] // HARDCODED!?!
  //public String[] mFieldNames;      // char     **mFieldNames;
  public IntPtr mFieldNames;
  public int mNumFields;
  [MarshalAs(UnmanagedType.LPTStr)]
  public String mFilter;
  [MarshalAs(UnmanagedType.LPTStr)]
  public String mSort;
  public int mOffset;
  public int mMaxRecords;
  //[MarshalAs(UnmanagedType.LPTStr)]
  public IntPtr mTargetRecordFilter;
  public int mSurroundingRecordsCount;
  public IntPtr mOwnerIds;
  public int mNumOwnerIds;
  public gsi_bool mCacheFlag;
 }

  [DllImport("saketestd.dll")]
  unsafe static extern void* sakeSearchForRecords(
   IntPtr sake,
   IntPtr input, //SAKESearchForRecordsInput *
   SAKERequestCallback callback, //SAKERequestCallback 
   IntPtr userData);

  unsafe public bool sakeSearchForRecordsE() {
   bool ret = false;
   try {
    searchInput.mTableId = "bbdx_score";
    //searchInput.mFieldNames = mFieldNames.to;
    searchInput.mFilter = "num_ratings = 0 AND filestore > 0";
    searchInput.mSort = "";
    searchInput.mOffset = 0;
    searchInput.mMaxRecords = 1;
    //searchInput.mTargetRecordFilter = "";
    searchInput.mSurroundingRecordsCount = 0;
    searchInput.mOwnerIds = IntPtr.Zero;
    searchInput.mNumOwnerIds = 0;
    searchInput.mCacheFlag = true;

    int sakeSize = Marshal.SizeOf(sake);
    debug.AddLine(this.getMethodName() + ": sizeof(sake): " + sakeSize);
    IntPtr pSake = Marshal.AllocHGlobal(sakeSize);
    Marshal.StructureToPtr(sake, pSake, true);

    int inputSize = Marshal.SizeOf(searchInput);
    debug.AddLine(this.getMethodName() + ": sizeof(input): " + inputSize);
    IntPtr pInput = Marshal.AllocHGlobal(inputSize);
    Marshal.StructureToPtr(searchInput, pInput, true);

    IntPtr[] mFieldNamesPtr;
    int i;
    if (true) { // IntPtr[]
     mFieldNamesPtr = new IntPtr[mFieldNames.Length];
     i = 0;
     foreach (string str in mFieldNames) {
      mFieldNamesPtr[i++] = Marshal.StringToHGlobalAnsi(str);
     }
     //searchInput.mFieldNames = mFieldNamesPtr;
    } else {
     //searchInput.mFieldNames = mFieldNames;
    }
    searchInput.mNumFields = mFieldNames.Length;

    void* pRequestInternal = null;
     void* p = mFieldNamesPtr[0].ToPointer();
     searchInput.mFieldNames = (IntPtr)p;
     pRequestInternal = sakeSearchForRecords(
      pSake,
      pInput,
      new SAKERequestCallback(this.sakeSearchForRecordsCB),
      IntPtr.Zero
     );


    sake = (SAKEInternal)Marshal.PtrToStructure(pSake, typeof(SAKEInternal));
    if (searchRequest == null) {
     debug.AddLine(this.getMethodName() + ": mStartRequestResult: " + sake.mStartRequestResult);
    } else {
     ret = true;
     this.searchRequest = (SAKERequestInternal)Marshal.PtrToStructure(
      new IntPtr(pRequestInternal),
      typeof(SAKERequestInternal)
     );
     searchInput = (SAKESearchForRecordsInput)Marshal.PtrToStructure(
      pInput,
      typeof(SAKESearchForRecordsInput)
     );

     if (true) {
      i = 0;
      foreach (string str in mFieldNames) {
       Marshal.FreeHGlobal(mFieldNamesPtr[i++]);
      }
     }

     PrintStruct ps = new PrintStruct(sake);
     debug.AddLine(this.getMethodName() + ": sake: " + ps);
     ps = new PrintStruct(searchRequest);
     debug.AddLine(this.getMethodName() + ": searchRequest: " + ps.print_r());
     ps = new PrintStruct(searchInput);
     debug.AddLine(this.getMethodName() + ": searchInput: " + ps.print_r());
    }
    Marshal.FreeHGlobal(pSake);
    Marshal.FreeHGlobal(pInput);
   } catch (Exception ex) {
    debug.Text += ex.ToString();
   }
   return ret;
  }

回答1:

The best way to Marshal nasty string pointers, especially double pointers within a struct is to simply use an IntPtr.

public IntPtr mFieldNames;

This will Marshal correctly albeit with a not so useful type. However if you understand the structure of the IntPtr it's very easy to get the resulting strings out.

public static List<string> GetAllStrings(IntPtr ptr, int size) {
  var list = new List<string>();
  for ( int i = 0; i < size; i++ ) {
    var strPtr = (IntPtr)Marshal.PtrToStructure(ptr, typeof(IntPtr));
    list.Add(Marshal.PtrToStringUni(strPtr));
    ptr = new IntPtr(ptr.ToInt64()+IntPtr.Size);
  }
  return list;
}

The only real downside is that you will have to manually free the memory



回答2:

A better way is simply to use unsafe code with sbyte which is the same as c-char (-128 to 127) 1 byte. You can write yourself some extern functions like alloc_txt, free_txt,etc.. for allocating and freeing from the heap. Mostly when I write with interop I do use unsafe code because IntPtr gets you the address but you still have to use extern functions to get members in the structure it points to or if a primitive have to Marshal methods to extract what the value is.

The only time you have to declare a c# structure as unsafe is if you are using actual pointers which you are not but using MarshalAs instead. I still would prefer you use unsafe pointers via MarshalAs(UnmanagedType.?) which allows you do deal with the members directly.

[Struct(Layout.Sequential)]
public unsafe struct SAKESearchForRecordsInput
{
sbyte*mTableId;
sbyte**mFieldNames;
int mNumFields;
sbyte*mFilter;
sbyte*mSort;
int mOffset;
int mMaxRecords;
char*mTargetRecordFilter;
int mSurroundingRecordsCount;
int*mOwnerIds;
int mNumOwnerIds;
bool mCacheFlag;//?don't know what the typedef for the bytes
};