I just spent the day figuring out how to Marshal arrays of bytes between C# and Nim (C# calling a DLL written in Nim) and wanted to share what I came up with can give people the chance to correct me. For some context, this is a Nim library that can be used natively or compiled to a DLL, the code presented will be an exported DLL function.
One of the Nim exports I have written looks like this:
proc transmitAndReceive(i2cAddress: byte, txdata: ptr UncheckedArray[byte], rxdata: var ptr UncheckedArray[byte], txlen, rxlen: int ): MyEnum {.exportc, dynlib, stdcall.} =
let convertedTx = @(toOpenArray(txdata, 0, txlen))
var convertedRx = newSeq[byte](rxlen)
result = myModule.transmitAndReceive(i2cAddress, convertedTx, convertedRx)
if rxlen > 0 and convertedRx.len >= rxlen:
rxdata = cast[ptr UncheckedArray[byte]](CoTaskMemAlloc(rxlen))
copyMem(rxdata, convertedRx[0].addr, rxlen)
proc CoTaskMemAlloc(n: int): pointer {.importc, dynlib:"ole32.dll".}'
The C# code for using the DLL:
[DllImport(dllname, EntryPoint = "transmitAndReceive", CallingConvention = CallingConvention.StdCall)]
public static extern MyEnum TransmitAndReceive(
byte i2cAddress,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U8, SizeParamIndex = 3)] byte[] txData,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U8, SizeParamIndex = 4)] out byte[] rxData,
int txlen, int rxlen);
So how do I use this?
byte[] getacq = new byte[] { 3 }; // Tells the thing I'm talking to, to return the data from channel 3
byte[] recvacq; // Where I will receive the data, Null here because no initialization or assignment.
TransmitAndReceive(0x42, getacq, out recvacq, getacq.Length, 5);
I pass the data I want to send (getacq) which is then Marshalled as a C style array that has length getacq.length (1). My Nim DLL then uses that information to create a Seq that is processed and transmitted, after all that happens the DLL allocs a new block of memory the size of rxlen, then copies that data from convertedRx which contains the response from the device in question. This should be safe because the Seq I make is of length rxlen as is the block of memory I alloc, so copyMem can not read past or overflow and I do not alloc if convertedRx got smaller in the call chain. (There is code missing that does Null checking for the alloc and returns an error). Once the call returns the Marshaller will convert the C style array to a .net array using the SizeParamIndex and recvacq becomes a valid not Null array, in the case of this code and array of len 5 with the first byte being a status and the next 4 a float32. Additionally when deemed appropriate .net will dealloc the array which I accidentally proved to myself with a typo.