返回资讯中心
外部精选
软件工程
#开发者实时流

Async FFI Framework for Rust <-> C# Interop

The biggest challenge in exposing C# data to Rust was preventing the Garbage Collector from moving or freeing them while they’re still in use. C# objects are heap-allocated. In languages such as C/C++ or Rust, heap-allo…

Hacker News 最新kbr-2026/11/10 · 今天9 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

The biggest challenge in exposing C# data to Rust was preventing the Garbage Collector from moving or freeing them while they’re still in use. C# objects are heap-allocated. In languages such as C/C++ or Rust, heap-allocated objects usually keep a stable address in memory (unless manually reallocated, think of vector resizing for example). C#, being equipped with an advanced GC, does not provide this guarantee: objects can be moved on the heap at any time! The GC can fire at arbitrary moments and mess up with the pointers. If staying in safe C#, this is not the problem because the GC switches all the managed pointers from the old location to the new one. The problem arises when using unmanaged pointers, which is the only way to pass pointers across FFI. The GC does not trace them, so it does not update them! Now, the usual way to pin memory in C# and prevent moving is to create a GCHandle. It has the following features we like:

GCHandle is thus a perfect solution for cases when the lifetime is dynamic and not known up front. Most notably, this is the case of asynchronous calls. However, we noticed it’s overkill for simpler cases when the lifetime is static and known up front – the synchronous calls case.

The key observation we made: if we are inside a Rust function that was called from a C# call stack, we are guaranteed that the whole C# call stack is alive! Well, this is the foundational assumption of automatic, i.e. stack based, memory management. If we add another observation that the stack is immovable (i.e. GC cannot move items inside stack frames), we get a foundational idea of our solution – stack pinning.

Wait, you said stack. But you also said that C# objects live on the heap. How to reconcile these two?

Well, objects do live on the heap, but C# operates on objects via references, which – if kept as variables – do live on the stack*. So: objects can be moved on the heap freely whenever the GC desires, but references to those objects sit on the stack still. OK, great! Then those references are our Holy Grail. We pass pointers to those references to Rust, and whenever we dereference those pointers in the synchronous call, they will still point to our references. The validity of those references, being managed pointers, is guaranteed by the GC itself – they are updated whenever the pointee is moved in memory.

Technically, we do it using Unsafe.AsPointer combined with the ref keyword. If we passed the reference to Unsafe.AsPointer without using ref, we would end up converting the reference to the heap object into an unmanaged pointer to a heap object – leaving us with a potentially dangling pointer (if GC fires and moves the object). With ref, we convert a reference to the stack reference to the heap object into an unmanaged pointer, which is exactly what we wanted.

There’s one catch though: the compiler technically could decide that once we created unmanaged pointers to stack-living references, those references are no longer needed (because they are untracked). It could, for instance, reuse their memory in the stack frame by putting some other data there. We need to prevent it, and for that we use GC.KeepAlive(). This is purely information for the compiler, For stack values, this has zero runtime overhead. It just prevents the compiler from reusing the stack frame slot prematurely.

*Actually, they can also live in CPU registers if the optimizer kicks in, but we can easily prevent it by taking a pointer to the variable. Since register variables cannot be pointed to, the optimizer must leave them on the stack.

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →