How to call C# from rust?

I am trying to do c#/rust interop, and calling rust from c# is ok by far, but calling c# from rust would lead to a crash.I just pass a struct with delegate field to rust, and call the delegate from rust.

C# code:

[StructLayout(LayoutKind.Sequential)]
public class UnityBinding
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void Test();

//[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
//delegate void UnityDebugLog(IntPtr data, int length);

private Test test;

//private UnityDebugLog debugLog;

//[MonoPInvokeCallback(typeof(UnityDebugLog))]
public static void DebugLog(IntPtr data, int length)
{
    Debug.Log("debug log called");
    //var message = Marshal.PtrToStringAuto(data, length);
    //Debug.Log(message);
}

[MonoPInvokeCallbackAttribute(typeof(Test))]
public static void MyTest()
{
    Debug.Log("Test called");
}

public static UnityBinding Create()
{
    return new UnityBinding
    {
        test = MyTest,
        //debugLog = DebugLog,
    };
}
}

Rust code:

#[repr(C)]
pub struct UnityBinding {
    pub(crate) test: extern "C" fn(),
    //pub(crate) debug_log: extern "C" fn(*const c_void, i32),
}

pub extern "C" fn init_unity_engine(binding: UnityBinding) {
    println!("before debug_log");
    (binding.test)();
    println!("after debug_log");
}

I can't figure out what's wrong here, I really need some help, thanks.

How are you obtaining the UnityBinding instance in Rust? It looks like you are passing it to init_unity_engine by-value, but C# classes are implicitly pointers. Shouldn't it be a struct instead?

1 Like

I'd be worried about by-value vs. by-ref semantics here, so either init_unity_engine ought to be taking something by reference (e.g., Option<&'_ UnityBinding>), or your class should be a struct instead (see also: Reference types - C# Reference | Microsoft Docs)

A good way to make sure you get that right is to also deal with a good old integer field, set it to a small number (say, 42_u32), and try and read that from Rust. If you got indirection wrong, you are likely to be reading the address of your class instead, which is likely not to be a small number

1 Like

Hello,

I don't know anything about C# but here is how I would do it in C++, hope it will help you for your C# integration. First, you need to read some documentation about Lazy Static in Rust (lazy_static - Rust).

The main Idea is the following, your Rust will have a Struct (UnityBinder) which will hold a void pointer to your C++/C# Struct. Then I will use FFI (FFI - The Rustonomicon) to call C/C# code from Rust, cast the void pointer to your C++/C# struct pointer and call the associated function.

unity_binder.rs:

use std::ffi::c_void;

type c_unity_binder = *const c_void; 

lazy_static! {
    pub static ref UnityBinding: Mutex<UnityBinding> = Mutex::new(UnityBinding::new());
}

pub struct UnityBinding {
    unity_binder: *const c_void;
}

impl UnityBinding {
    
    fn new(id: u16) -> UnityBinder { // I'm doing it quick here but you should return a Result
        let unity_binder = unsafe { init_binder(id) };
        UnityBinder { unity_binder } // Before create the binder you should check if the ptr is null or not
    }

    pub fn test(&self) {
        rust_to_c_test(self.unity_binder);
    }

    pub fn debug_log(&self, param1: *const c_void, id: i32) {
        rust_to_c_debug_log(self.unity_binder, param1, id);
    }
} 

extern "C" {
    pub fn rust_to_c_init_binder(id: u16) -> unity_binder; //put the param you need to init your binder, return a pointer to your c struct
    pub fn rust_to_c_test(binder: c_unity_binder);
    pub fn rust_to_c_debug_log(binder: c_unity_binder, param1: *const c_void, id: i32);
}

unity_binder_callback.cpp :

extern "C" {
    void* rust_to_c_init_binder(uint16_t id) {
        void* struct_ptr = your_function_to_get_struct_ptr(id);
        return struct_ptr;
    }

    void rust_to_c_test(void* binder) {
        if (!binder) return;
        // Do what you what you want here, example:
        binder->test();
    }

    void rust_to_c_debug_log(void* binder, void* param1, uint32_t id) {
        if (!binder) return;
        //do what you want here, example:
        binder->DebugLog(param1, id);
    }

Of course I let you handle all the case you need to handle when you are managing pointer and the import as well I might have forgotten some of them. I just wrote this code quickly to give an idea how it would be possibly done.

Hope this might help you ! :smiley:

1 Like

Yes, struct can do the magic, thanks.