Rust: 可以从 C 调用的 Rust 函数

创建于 2012-02-02  ·  16评论  ·  资料来源: rust-lang/rust

我们现在有很多场景,创建绑定的人希望能够提供一个 C 函数可以调用的回调。 当前的解决方案是编写一个原生 C 函数,该函数使用一些未指定的内部 API 将消息发送回 Rust 代码。 理想情况下,它不涉及编写 C 代码。

这是在 Rust 中创建可以从 C 代码调用的函数的最小解决方案。 要点是:1)我们还有另一种函数声明,2)这个函数不能从 Rust 代码中调用,3)它的值可以作为一个不透明的不安全指针,4)它在堆栈切换魔法中烘焙并适应从 C ABI 到 Rust ABI。

C-to-Rust(地壳)函数的声明:

crust fn callback(a: *whatever) {
}

获取指向 C ABI 函数的不安全指针:

let callbackptr: *u8 = callback;

我们也可以专门为此目的定义一些类型。

编译器实现:

它主要是直截了当的,但 trans 变得丑陋。 在 trans 中,我们需要做的基本上与我们对原生 mod 函数所做的相反:

  • 使用声明的签名生成 C ABI 函数
  • 生成一个在结构中接受 C 参数的填充函数
  • C 函数将参数填充到结构中
  • C 函数使用参数结构和 shim 函数的地址调用 upcall_call_shim_on_rust_stack
  • 使用声明的签名生成 Rust ABI 函数
  • shim 函数从结构体中取出参数并调用 Rust 函数

运行时实现:

运行时必须以几种方式进行更改才能实现此目的:

  • 切换回 Rust 堆栈的新调用
  • 任务需要维护一堆 Rust 上下文和 C 上下文
  • 重新进入 Rust 堆栈后需要一个策略来处理失败
  • 需要一个策略来处理重新进入 Rust 堆栈后的屈服

失败:

我们不能在重新进入 Rust 堆栈后简单地抛出异常,因为不能保证本地代码可以用 C++ 异常展开。 在这种情况下,Go 语言显然会跳过所有本地帧,一路泄漏所有内容。 相反,我们将中止——如果用户想要避免灾难性失败,他们应该使用他们的 Rust 回调来调度消息并立即返回。

产量:

如果不改变我们处理 C 堆栈的方式,我们就不能允许 Rust 函数在从 C 代码重新进入 Rust 堆栈后上下文切换到调度程序。 我看到两种解决方案:

1) 重新进入 Rust 堆栈和简单地阻塞后,产量是不同的。 想要执行此操作的任务应确保它们有自己的调度程序 (#1721)。
2) 任务不是使用调度程序的堆栈运行本机代码,而是从位于每个调度程序的池中检出 C 堆栈。 每次任务重新进入 C 堆栈时,它会检查它是否已经有一个并重用它,否则它将从调度程序请求一个新的。 这将允许 Rust 代码始终正常产生而不会占用调度程序。

我更喜欢第二种选择。

另见#1508

A-debuginfo A-runtime A-typesystem E-easy

最有用的评论

请原谅这个复活,但是这个问题是从一个 y-combinator 和其他一些网站链接的,我最近有一个菜鸟问这个问题,所以我注意到,根据这个问题和后来的变化,从 C 调用 Rust很简单

#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
    "Hello, world!\0".as_ptr()
}
#include "stdio.h"
const char *hello_rust(void);
int main(void) {
    printf("%.32s\n", hello_rust());
}

所有16条评论

很抱歉跳到这里,但我想强调的是,调用 C 函数应该很快,就像在 _blazing_ 中一样。 如果我想使用 Allegro、SDL 或 Opengl 等 C 库编写 rust 游戏,这是必不可少的。 否则游戏会在大量C调用的渲染代码中变慢,这是不能接受的。 默认的带有 cgo 的 Go 语言编译器就有这样的问题。

所以我更喜欢快速的解决方案,即使它可能会限制 Rust 端的功能可以做什么。

另外,使用“native fn”代替“crust fn”不是一个想法,还是有其他计划的含义?

@beoran你有这个落后。 我们正在谈论 C 调用 Rust。 从 Rust 调用 C 函数已经相当快了(可以做得更快一些)。

好的我明白了。 有什么方法可以帮助我加快从 Rust 调用 C 的速度?

@beoran正如我所说的,它与这个问题有点正交……但你能做的最好的事情可能是建立一个基准,显示性能如何不足。 :)

好的,当我对 Allegro 进行足够多的包装以比较从 rust Rust 调用它与从 C 调用它的开销时,我会这样做。我现在先不理会这个问题,一旦我有了新的问题,我会打开一个新的基准。

我们是否可以通过让 C 函数直接写入 Rust 堆栈来避免参数的副本之一,就像 Rust->C 调用当前所做的那样?

我希望通过内联消除来自 shim 结构和进入 rust 函数参数的参数的最终副本。 我不确定这是否是你所指的。

我相信我们的 C 调用当前将参数复制到 Rust 堆栈上的结构中,然后将该结构复制到 C 堆栈中。

@pcwalton Rust- >C 当前不直接写入 C 堆栈,因为这是特定于 i386 的。 我想避免必须编写特定于特定调用约定的代码,即使以牺牲一些性能为代价,以便 64 位工作。 (我认为这样的优化现在可能有意义,不过——特别是#1402 指出 LLVM 并没有真正完全处理调用约定_无论如何_)

读完栈切换函数后,arg struct 根本没有跨栈复制,只是将指向前一个栈的指针传递给在新栈上运行的函数,这很有意义。

arg 结构不会被复制,但 shim 函数会从中加载值并将它们重新推送到新堆栈上。 用于将参数值直接写入目标堆栈的旧代码。 这在 i386 上是有意义的,但在 x86_64 上,弄清楚哪些值将进入堆栈等要复杂得多。

经过一些测试,我发现外壳功能很难保证它们不会失效。 当前发生的情况是,当顶级任务(如 main)失败时,每个任务都会被告知失败,因此一旦回调尝试发送消息(或从发送消息返回时),它最终可能会失败并导致运行时异常中止。

我想我们可以更改 rust_task 以在任务重新进入 Rust 堆栈后忽略终止请求。 对于实现事件循环的任务,他们可以查看某个监视器端口,寻找指示运行时失败的消息,并找出如何正常终止。

那么当顶级任务失败时,它会将错误传播给它的子任务吗? 我想我不明白我们的错误传播模型,我认为它是从叶子开始的。 似乎在 C 堆栈上运行的代码应该能够在无监督任务或类似任务中运行。

它基本上就像“main”受内核监督一样,所以如果 main 失败,一切都会失败。

我说这已经完成了。 有一点清理,我已经为剩余的问题提交了单独的错误。

请原谅这个复活,但是这个问题是从一个 y-combinator 和其他一些网站链接的,我最近有一个菜鸟问这个问题,所以我注意到,根据这个问题和后来的变化,从 C 调用 Rust很简单

#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
    "Hello, world!\0".as_ptr()
}
#include "stdio.h"
const char *hello_rust(void);
int main(void) {
    printf("%.32s\n", hello_rust());
}
此页面是否有帮助?
0 / 5 - 0 等级