4

I'm trying to pass a C# method to Rust to be used as a callback. I've managed to pass a static function and it works fine (see below).

Now I'd like to call an instance method, which means that the trigger function below should also receive an opaque (libc::c_void) this pointer. How can I get the IntPtr which Rust should pass as the instance pointer to the callback function?

C#

    class Program
    {
        delegate void PrintFn(int x);

        public static void Main(string[] args)
        {
            PrintFn fn = Print;
            var ptr = Marshal.GetFunctionPointerForDelegate(fn);
            IntPtr handle = IntPtr.Zero;
            create(ptr, out handle);
            trigger(handle, 3);
        }

        public static void Print(int x)
        {
            Console.WriteLine($"C#: {x}");
        }

        [DllImport(@"/path/to/rust/dll")]
        public static extern int create(IntPtr readFn, out IntPtr callback);

        [DllImport(@"/path/to/rust/dll")]
        public static extern int trigger(IntPtr handle, int x);
    }

Rust:

use std::boxed::Box;

pub struct RustCallback {
    callback: extern "stdcall" fn(i32),
}

impl RustCallback {
    fn new(callback: extern "stdcall" fn(i32)) -> RustCallback {
        RustCallback {
            callback: read_fn,
        }
    }
}

pub fn set_output_arg<T>(out: *mut T, value: T) {
    unsafe { *out.as_mut().unwrap() = value };
}

#[no_mangle]
pub extern "C" fn create(callback: extern "stdcall" fn(i32), sc: *mut *mut RustCallback)  -> u32 {
    set_output_arg(sc, Box::into_raw(Box::new(RustCallback::new(callback))));
    0
}

#[no_mangle]
pub extern "C" fn trigger(sc: *mut RustCallback, x: i32)  ->  u32 {
    let sc = unsafe { sc.as_mut().unwrap() };
    let f = sc.read_fn;
    f(x);
    0
}
Shmoopy
  • 5,334
  • 4
  • 36
  • 72
  • 1
    If I understand correctly - you don't need to do this. Just pass pointer to instance method exactly the same way you do for static - .net will handle it for you. – Evk Apr 12 '18 at 15:56
  • @Evk It works! thanks, didn't cross my mind to even try without the instance pointer. Any idea how that magic works? Does the pointer passed to Rust point to a wrapper function created by the CLR which remembers the this pointer and pushes it to the stack before calling the instance method? – Shmoopy Apr 12 '18 at 16:15
  • Basically yes. .net delegate has Target property, so it's not purely a function pointer. GetFunctionPointerForDelegate preserves this semantics. You can read a bit more here: https://stackoverflow.com/q/20600380/5311735 – Evk Apr 12 '18 at 16:24
  • Also pay attention to keep delegate alive, otherwise garbage collector might collect it before (or during) rust executes it, which will lead to surprising results. .NET cannot know that delegate is used by unmanaged code, so you should take care of that yourself. – Evk Apr 12 '18 at 16:26
  • Though keeping alive is important only if callback can be executed later than you pass it. If rust only executes callback during the same function you pass it in (doesn't store it for later use) - .net should keep it alive itself. – Evk Apr 12 '18 at 16:32
  • @Evk isn’t that risky ? Can’t the delegate or the instance move around the heap even if neither of them was collected? – Shmoopy Apr 12 '18 at 16:42
  • They can, but this is handled correctly by .net, as far as I know. A bit more info here: https://stackoverflow.com/q/45786857/5311735 – Evk Apr 12 '18 at 17:11
  • Usually, you don't even need to call `Marshal.GetFunctionPointerForDelegate` manually; you can just declare the function pointer argument with a delegate type instead of `IntPtr`. – Francis Gagné Apr 15 '18 at 00:37
  • One question - Where does read_fn come from? – Alexandra Danith Ansley Nov 25 '21 at 18:46

1 Answers1

0

I was dealing with the same code. I found a simpler way than the one you expose, look:

fn operation(number: i32, f: &dyn Fn(i32) -> i32) {
    f(number);
}
#[no_mangle]
fn print(number: i32) {
    println!("Rust: {}", number);
}
#[no_mangle]
pub extern "C" fn c_print(number: i32, callback: unsafe extern "C" fn(i32) -> i32) {
    operation(number, &|n| unsafe { callback(n) });
}

Then, in C#:

delegate void PrintFn(int number);

public static void Run()
{
    int number = 1234;

    // passing a C# function as parameter of Rust function
    c_print(number, Print);

    // passing a Rust function as callback inside the same Rust function
    print(number);
}

static void Print(int number)
{
    Console.WriteLine($"C#: {number}\n");
}

[DllImport(RLIB)] static extern void c_print(int number, PrintFn fn);
[DllImport(RLIB)] static extern void print(int number);

When I started to deal with this, look for: «How to use from C# a Rust function that has another function as a parameter?»