我正在Rust中编写一个R包,它通过C API接口与R通信。
对我来说,一个基本问题似乎很棘手,那就是内存管理。
因此,首先让我简要解释一下我的Rust程序是如何与R.通信的
首先,在R端,它用.Call()
调用C动态库。然后将C库链接到具有C兼容ABI的Rust静态库。
R脚本正在做一些简单的工作,比如输入验证,并根据输入决定调用哪个C函数。然后,C程序将调用传递给底层的Rust函数,这些函数被公开为C兼容函数。
到目前为止还很清楚,一旦Rust完成计算并需要将结果发回,就会变得棘手。
一种选择是直接调用Rust端的Rf_allocXXX
函数,并将结果存储在其中。然后将R对象的原始指针传回C,然后传给R。
但我不清楚这是否会导致内存泄漏。
在我看来,如果Rust调用Rf_allocXXX
,则在Rust程序的堆内存上创建新的R对象(SEXP
(。当将原始指针传出时,Rust不会破坏对象。但是接下来会发生什么呢?
请注意,Rust以这种方式创建的SEXP
直接传递回C和R。没有重新分配。因此,我似乎很不清楚这个SEXP
是否会被R的GC正确释放。
一个相关问题,
我从Rcpp
的源代码中注意到,它似乎只是调用R的C API来创建它的各种向量,这些向量是SEXP
的包装器。
但我还不清楚它是如何处理内存管理的。它只是简单地将SEXP
对象转回,R将正确地处理GC吗?
这一切(一如既往(都在编写R扩展中(一如既往地不是最容易找到的……(
简言之,当您调用R扩展包时,您基本上需要调用R的API及其Calloc()
和Free()
例程(和变体(。为什么?因为返回到R的任何东西都将成为R对象,与所有其他R对象无法区分,并且行为相同。包括,非常重要的是,当涉及到垃圾收集时。
唯一的方法是通过R自己的分配器。所以Rcpp使用了它,并创建了实际上无法区分的对象。这让一切都运转起来。
R> Rcpp::cppFunction("IntegerVector foo() {
+ IntegerVector v = {1, 2, 3}; return v; }")
R> foo()
[1] 1 2 3
R> identical(foo(), c(1L, 2L, 3L))
[1] TRUE
R> identical(foo(), 1:3)
[1] TRUE
R>
但作为第一步,您可以在Rust中计算结果,然后支付一次性转换成本(从您的对象转换为SEXP
R期望的.Call()
("先走后跑;等等。锈蚀的绑定会很酷。我想你知道杰伦和其他人已经做了一些工作吗?
此问题的更新:
根据Dirik的回答,在阅读了Writing R Extensions
中的内存分配部分后,我现在意识到
- 如果一个可能使用其他语言的外部程序创建了
SEXP
对象通过R_allocXXX
接口或Rcpp
中的等价项,然后R负责释放存储器,- 在
.C()
、.Call()
和.External()
的调用结束时 - 或者当出现错误时
- 在
- 但是,如果通过
malloc
或类似方式手动创建R对象界面,用户有责任释放内存,无论无论该对象是在C、Rcpp
或其他外部程序中创建的
因此,如果我们假设R在释放内存之前没有恐慌,那么在大多数情况下在某些情况下,我们应该使用R_allocXXX
绑定来创建SEXP
将无内存泄漏。一旦对象转到R,它就安全了。
但在处理一般的外部功能接口(FFI(时仍必须谨慎,
- 如果我们手动分配内存并向C接口返回一个C对象,那么在转换C对象之前,我们需要确保FFI双方都不恐慌至CCD_ 25。否则,内存不会被释放
- 类似地,这适用于当我们在C程序中手动分配存储器时,并将对象发送到外部程序
- 在任何情况下,手动分配的内存都需要手动释放
因此,通常情况下,为了内存安全,我们应该尽可能使用R_allocXXX
接口。