4

Can you help me use a C function, from F#, that expects a callback? I am using F#, mono and Arch Linux, on the ARM processor based Raspberry Pi 2.

My original problem, understanding the signature to use for the callback, was answered which you can read for more context.

I have reached an impasse. When using the library, the program crashes with a segmentation fault.

I believe that this is because of a marshalling mismatch. The "the clr supports only the std calling convention for marshalling function pointers" according to the book Expert F# 3.0. The CLR assumes that the callee will clean the stack (StdCall) and the C library expects that the caller will clean the stack (Cdecl) according to the definition on MSDN. Hence the memory leak.

Sadly, I can find no way to make the CLR use Cdecl for this delegate nor for C to use StdCall on an ARM processor. I haven't written the C library but I do have access to the source code.

Here is the function signature in C

int wiringPiISR (int pin, int edgeType,  void (*function)(void)) ;

and part of my F# code:

type ISRCallback = delegate of unit -> unit

[<DllImport(wiringPiLib, EntryPoint = "wiringPiISR", CallingConvention = CallingConvention.Cdecl, SetLastError=true)>]
    extern int wiringPiISR(int pin, int mode, [<MarshalAs(UnmanagedType.FunctionPtr)>]ISRCallback callBack);

Decorating the delegate with [<UnmanagedFunctionPointer(CallingConvention.Cdecl)>] doesn't help. The compiler silently ignores it, which I think is how these attributes are designed to work.

Is there another way to do this - perhaps editing the C code, or wrapping it in another way? (C++ perhaps?)

Note: there is an interesting article on the mono-project website titled Interop with Native Libraries

In the meantime, I have written a little loop to poll the GPIO pins. Not ideal but it works.

EDIT: I was wrong in my memory leak assumption but, thanks to the comments and the research it prompted, I learnt a lot and found answers.

Community
  • 1
  • 1
Jack Chidley
  • 131
  • 9
  • 4
    `CallingConvention = CallingConvention.Cdecl` is exactly the correct solution (I doubt you want `SetLastError=true` but it shouldn't cause any problems). How are you actually invoking `wiringPiISR`? The most likely issue is that you are not correctly ensuring the lifetime of the delegate you pass in. – ildjarn Jun 15 '16 at 18:34
  • `let callback : ISRCallback = ISRCallback( fun () -> let now = NodaTime.SystemClock.Instance.Now hotWaterAgent.Update (agentEvent, now) )` the `now = NodaTime.SystemClock.Instance.Now hotWaterAgent.Update (agentEvent, now)` part must be OK because that is exactly how the working version does things – Jack Chidley Jun 15 '16 at 18:40
  • 4
    To quote the answerer of your previous question, "*You should keep in mind that delegates in .NET are subject to garbage collection. It is the responsibility of the developer to ensure that the delegate doesn't "go out of scope" until your native library no longer needs the function callback.*" You're not doing this, so when the WiringPi library tries to call your function, it's been GCed and you get the segfault. – ildjarn Jun 15 '16 at 18:42
  • Hmmm... thus betraying my inexperience as I have no idea how to do that! – Jack Chidley Jun 15 '16 at 19:22
  • To keep an object from garbage collection, put a reference to it in a field of some object that you know won't be collected itself. One obvious solution is a static field, but that may not be ideal. – Fyodor Soikin Jun 16 '16 at 04:46
  • 1
    To keep a referenced object from garbage collection, call GC.KeepAlive with the reference. I like this best, because it's very obvious what you want to achieve. But Fyodor Soikin's suggestion also works of course. – Bent Tranberg Jun 17 '16 at 12:47
  • 2
    GC.KeepAlive keeps the object referenced only up to the KeepAlive statement; past that it may be eligible for collection. For callbacks you likely need to manually keep a reference while the callback may be called. – Curt Nichols Jun 17 '16 at 18:54

1 Answers1

1

The segmentation fault, as noted in the comments and as suggested in the answer to my original question, is caused by the garbage collection of the callback function.

Adding GC.StayAlive (MyFunctionDoingCallBack) at the right place in my code makes it clear to anyone reviewing the code - me - when it is safe for the callback garbage collected. This solution was easier than working out why the callback was going out of scope and thus how to keep it in scope. I realise this reflects badly on me as a programmer ;-(.

This answer explains why SetLastError=false is correct. The native code definitely does not return a Win32 Error, it's running on Linux anyway, so I'd only be getting garbage from the error code.

Setting CallingConvention.Cdecl on the DLLImport attribute works. Perhaps the quote "The clr supports only the std calling convention for marshalling function pointers" is wrong for my system (mono on Linux) or I misunderstood the quote.

Setting [<UnmanagedFunctionPointer(CallingConvention.Cdecl)>] on the delegate appears to be redundant as is EntryPoint = "wiringPiISR" part of the DLLImport attribute. For clarity I removed both.

There is a series of articles about DDLImport and DLLExport on the OldNewThing blog at MSDN

Thanks @FyodorSoikin, @ildjarn, @BentTranberg, @CurtNichols and @vcsjones

Community
  • 1
  • 1
Jack Chidley
  • 131
  • 9