We have a native code SDK which predominantly uses the C/C++ size_t
type for things like array sizes. We additionally provide a .NET wrapper (written in C#) which uses PInvoke to invoke the native code, for those that want to integrate our SDK into their .NET app.
.NET has the System.UIntPtr
type which pairs perfectly with size_t
functionally, and functionally everything works as expected. Some of the C# structures provided to the native side contain System.UIntPtr
types and they're exposed to consumers of the .NET API which requires them to work with System.UIntPtr
types. The problem is that System.UIntPtr
does not interoperate well with typical integer types in .NET. Casts are required and various "basic" things like comparisons to integers/literals don't work without more casting.
We tried declaring the exported size_t
params as uint
and applying the MarshalAsAttribute(UnmanagedType.SysUInt)
but that results in a runtime error for invalid marshaling. For example:
[DllImport("Native.dll", EntryPoint = "GetVersion")]
private static extern System.Int32 GetVersion(
[Out, MarshalAs(UnmanagedType.LPStr, SizeParamIndex = 1)]
StringBuilder strVersion,
[In, MarshalAs(UnmanagedType.SysUInt)]
uint uiVersionSize
);
Calling GetVersion in C# passing a uint for the 2nd param results in this marshal error at runtime:
System.Runtime.InteropServices.MarshalDirectiveException: Cannot marshal 'parameter #2': Invalid managed/unmanaged type combination (Int32/UInt32 must be paired with I4, U4, or Error).
We could create facade wrappers which expose 'int' types in .NET and internally do the casting to System.UIntPtr
for native-compatible classes, but (a) we worry about performance of copying the buffers (which could be very large) between near-duplicate classes and (b) it's a bunch of work.
Any suggestions on how to PInvoke with size_t
types while maintaining a convenient API in .NET?
Here's a sample of one case which is effectively the same as our real code but with simplified/stripped names. NOTE This code is derived from our production code by hand. It compiles for me, but I've not run it.
Native (C/C++) code:
#ifdef __cplusplus
extern "C"
{
#endif
enum Flags
{
DEFAULT_FLAGS = 0x00,
LEVEL_1 = 0x01,
};
struct Options
{
Flags flags;
size_t a;
size_t b;
size_t c;
};
int __declspec(dllexport) __stdcall InitOptions(
Options * const pOptions)
{
if(pOptions == nullptr)
{
return(-1);
}
pOptions->flags = DEFAULT_FLAGS;
pOptions->a = 1234;
pOptions->b = static_cast<size_t>(0xFFFFFFFF);
pOptions->c = (1024 * 1024 * 1234);
return(0);
}
#ifdef __cplusplus
}
#endif
Managed (C#) Code: (This should to repro the incorrect marshalling. Changing the fields a, b, and c in the struct to type UIntPtr makes it function properly.
using System;
using System.Runtime.InteropServices;
namespace Test
{
public enum Flags
{
DEFAULT_FLAGS = 0x00,
LEVEL_1 = 0x01,
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct Options
{
public Flags flags;
public uint a;
public uint b;
public uint c;
}
public class Test
{
[DllImport("my.dll", EntryPoint = "InitOptions", CallingConvention = CallingConvention.StdCall)]
internal static extern Int32 InitOptions(
[In, Out]
ref Options options
);
static void Main(string[] args)
{
Options options = new Options
{
flags = DEFAULT_FLAGS,
a = 111,
b = 222,
c = (1024 * 1024 * 1)
};
Int32 nResultCode = InitOptions(
ref options
);
if(nResultCode != 0)
{
System.Console.Error.WriteLine("Failed to initialize options.");
}
if( options.flags != DEFAULT_FLAGS
|| options.a != 1234
|| options.b != static_cast<size_t>(-1)
|| options.c != (1024 * 1024 * 1234) )
{
System.Console.Error.WriteLine("Options initialization failed.");
}
}
}
}
I tried changing the enum field in the managed struct to a int type and it still doesn't work.
I'll test more with size_t function params next.