2

The following example illustrates what I am looking to do. The operation function receives a value and a function as a parameter, then returns the execution of the function. So far everything is perfect. Now I would like to use that function operation in a library to consume in C#.

fn operation(number: i32, f: &dyn Fn(i32) -> i32) -> i32 {
    f(number)
}
fn square(number: i32) -> i32 {
    number * number
}
fn cube(number: i32) -> i32 {
    number * number * number
}

fn test() {// passing a function as parameter, OK :)
    println!("{}", operation(5, &square));
    println!("{}", operation(7, &square));
    println!("{}", operation(3, &cube));
}

// Tried without success :_(
/*
#[no_mangle] 
pub extern "C" c_operation(number: i32, f: &dyn Fn(i32) -> i32) -> *mut i32 {
    unsafe {
     &mut f(number)
    }
}
*/

What should the right signature of the function c_operation?

I think that the following C# code should work when the signature is well defined in the library.

const string RSTLIB = "../../PathOfDLL";

public delegate int Fn(int number);

[DllImport(RSTLIB)] static extern int c_operation(int x, Fn fn);

int Square(int number) => number * number;
int Cube(int number) => number * number * number;

void Test()
{
    Console.WriteLine("{0}", c_operation(5, Square));
    Console.WriteLine("{0}", c_operation(7, Square));
    Console.WriteLine("{0}", c_operation(3, Cube));
}

I appreciate your attention

Infosunny
  • 459
  • 4
  • 17

3 Answers3

3

For use from C, one way to declare c_operation and define it in terms of operation would be something like:

#[no_mangle]
pub extern "C" fn c_operation(
    number: i32,
    f: unsafe extern "C" fn(i32) -> i32
) -> i32 {
    operation(number, &|n| unsafe { f(n) })
}
user4815162342
  • 141,790
  • 18
  • 296
  • 355
3

dyn pointers are not FFI safe, so they cannot be passed through extern "C" barriers. You actually get a warning about this:

warning: extern fn uses type dyn Fn(i32) -> i32, which is not FFI-safe.

However you can pass a plain function pointer, as long as it is also extern "C", of course):

#[no_mangle] 
pub extern "C" fn c_operation(number: i32, f: extern "C" fn(i32) -> i32) -> *mut i32 {
    unsafe {
        &mut f(number)
    }
}

(You are returning a dangling pointer to a local temporary, but I guess that is just because you are writing dummy code.)

Then, from C, you can use plain functions, but there is no place for the closure data. That would be an issue for C or C++, but not for C#, because it does some magic that creates raw plain function pointers from any delegate, as long as you decorate the delegate type properly:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int Fn(int number)

Then, your c_operation() declaration should just work, except that your return type in C# int while the Rust version returns a pointer. That corresponds to IntPtr:

[DllImport(RSTLIB)] static extern IntPtr c_operation(int x, Fn fn);

Also, note that if the c_operation() stores the pointer fn to call it later, then in C# you must keep the delegate object alive, either holding it in a variable, or allocating a GCHandle to it.

    Fn fSquare = Square;
    //keep fSquare somewhere
    c_operation(5, fSquare);

If you fail to do that, and the Garbage Collector collects the automatic temporary delgate (that the compiler created for you), and then the Rust code tries to call the stored function pointer... well, it will not be nice.

rodrigo
  • 94,151
  • 12
  • 143
  • 190
  • 3
    consider adding this knowledge to https://github.com/shepmaster/rust-ffi-omnibus – Stargateur Nov 23 '21 at 08:55
  • 1
    @Stargateur: I didn't know that page, it is quite nice, but I'm sorry, I'm not very familiar with that Jekyll thing this web is using, or many of the languages involved. But feel free to copy any piece of text from my answers here. – rodrigo Nov 23 '21 at 21:43
1

User4815162342's answer was exactly what I intended. I would like to add that it is surprising that you can pass a C# function as a parameter to a Rust function, and get a result. Likewise, we can from C# pass a Rust function to the same Rust library in a callback. To round off the matter I am going to show the result.

//  user4815162342 answer:
#[no_mangle]
pub extern "C" fn c_operation(number: i32, f: unsafe extern "C" fn(i32) -> i32) -> i32 {
    operation(number, &|n| unsafe { f(n) })
}

// by example
#[no_mangle]
fn cube(number: i32) -> i32 {
    number * number * number
}

The C# code:

const string RSTLIB = "...\release\rstlib.dll";

public delegate int Fn(int number);

[DllImport(RSTLIB)] static extern int c_operation(int number, Fn fn);
[DllImport(RSTLIB)] static extern int cube(int number);

int Square(int number) => number * number;
int Cube(int number) => number * number * number;

public void Test()
{
    // passing a C# function as parameter of Rust function
    Console.WriteLine("c_operation(5, Square) : {0}", c_operation(5, Square));
    Console.WriteLine("c_operation(3, Cube)   : {0}", c_operation(3, Cube));

    // passing a Rust function as callback inside the same Rust function
    Console.WriteLine("c_operation(5, cube)   : {0}", c_operation(5, cube));

    // Output
    // c_operation(5, Square) : 25
    // c_operation(3, Cube)   : 27
    // c_operation(5, cube)   : 125
}

Thanks for your attention.