0

This question is a more detailed version of another one

I'm trying to get a simple FFI communication between Rust and C#. The idea is to call a Rust function from C# and vice versa. Here's what I have:

public class Program
{
    [DllImport("D:\\Misc. CSharp\\NativeTesting\\Rust\\target\\debug\\test.dll")]
    public static extern void Test(IntPtr function);

    public static void TestFunction()
    {
        Console.WriteLine("Test!");
    }
    
    public delegate void TestDelegate();
    
    public static void Main()
    {
        Test(Marshal.GetFunctionPointerForDelegate((TestDelegate) TestFunction));
        Console.WriteLine("Done");
    }
}

This calls the Rust program:

#[no_mangle]
pub extern "C" fn Test(logger: &i8) {
    let function = logger.clone() as *const ();
    let function: fn() = unsafe { std::mem::transmute(function) };
    (function)();
}

This throws the following exception:

Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
Repeat 2 times:
--------------------------------
   at NativeTesting.Program.Test(IntPtr)
--------------------------------
   at NativeTesting.Program.Main()

I've looked through a ton of other FFI material, and haven't found any good solutions (reverse P/Invoke, linking the Rust to the C# dll, or using CLI), so please don't recommend any other tools to do this that I've mentioned.

Big_Bad_E
  • 947
  • 1
  • 12
  • 23
  • 1
    Do know what calling convention Rust uses. Getting that wrong will mess up your stack, at which point all sorts of bad things are possible – Flydog57 Oct 01 '22 at 17:43
  • Some things that jump out: you don't specify calling conventions anywhere; you don't keep the delegate instance alive, so it might get GC'd; I think the rust side needs to take a `*` pointer, not `&`? – canton7 Oct 01 '22 at 17:45
  • 1
    Have you got a simple example working, which doesn't pass parameters, say? – canton7 Oct 01 '22 at 17:46
  • @canton7 This is the simple example, there is no way around passing the IntPtr parameter. If you mean C# calling the Rust method, that works fine, the question is about calling the C# method from Rust. But testing it, you were right, I needed a * pointer not a & – Big_Bad_E Oct 01 '22 at 17:53
  • One thing that I see will be wrong: by doing `logger.clone()` you are *dereferencing* the `&i8` and effectively trying to use the `i8` value that the `IntPtr` *points* to and then using that to build a function pointer. Edit: yeah, you should just use a `*const usize` as the parameter. – kmdreko Oct 01 '22 at 17:56
  • @kmdreko I've heard that usize has a bunch of issues with FFI, and i8 works (for Windows at least, idk about other platforms I'll have to test), so I'm putting it as the answer for now. – Big_Bad_E Oct 01 '22 at 18:21
  • 1
    @Big_Bad_E you should ignore me mentioning `usize`, it'd be just as arbitrary as `i8` since you only use the pointer and not what it points to. Though maybe `*const ()` would express that intent better in Rust. – kmdreko Oct 01 '22 at 18:25
  • There is still the issue that you create a delegate instance (through that cast) but don't retain it. That's a race: if the GC gets to it before rust calls it, you'll crash. Either use [UnmanagedCallersOnly](https://devblogs.microsoft.com/dotnet/improvements-in-native-code-interop-in-net-5-0/) to avoid needing to create a delegate instance, or make sure the delegate instance is retained long enough (GC.KeepAlive for a single method; store it in a field if it needs to stay alive for longer) – canton7 Oct 01 '22 at 19:54

1 Answers1

1

The issue, as pointed out in the comments, is with the pointer. A * pointer is needed, not a & pointer.

New rust code:

#[no_mangle]
pub extern "C" fn Test(logger: *const i8) {
    let function: fn() = unsafe { std::mem::transmute(logger) };
    (function)();
}
Big_Bad_E
  • 947
  • 1
  • 12
  • 23