当外部程序通过 R 的 C API 接口调用"Rf_allocXXX"时,谁在管理内存?



我正在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中计算结果,然后支付一次性转换成本(从您的对象转换为SEXPR期望的.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接口。

最新更新