最后,我找到了 2 个解决方案来正确处理带有 C++ 对象的 R 保存/加载机制(数据持久性)。没有一个是完美的,但似乎总比什么都不做要好。很快,这些解决方案是(详情如下):
- 使用 R Raw 数据类型存储 C++ 对象。这具有内存高效的优点,但在实践中非常复杂。就个人而言,我不会将它用于大型项目,因为内存管理可能很乏味。当然,使用模板类可能会有一些改进(如果你有想法,请分享)。
- 将对象存储为 R 和 C++ 结构,并在重新加载后重建 C++ 对象。C++ 对象只构建一次(不是针对每个函数调用),但数据存储两次。这具有在大型项目中更容易实施的重大优势。
为了展示这些解决方案,我使用了一个稍微复杂一些的例子来强调第一个解决方案的复杂性。在本例中,我们的示例 C++ 对象是一种链表。
第一个解决方案:使用 R RAW 数据
这个想法(由@KarlForner 提出)是使用R Raw 数据类型来存储对象的内容。在实践中,这意味着:
- 使用 R 原始数据类型分配内存。
- 确保分配正确的内存量(这可能很复杂)。
- 使用placement new 在指定的内存地址(带有原始数据的地址)处创建C++ 对象。
这是文件“myobject.cpp”:
#include <Rinternals.h>
#include <new>
//Our example Object: A linked list.
class MyRawObject{
int type;
// Next object in the list.
bool hasnext;
public:
MyRawObject(int t):type(t), hasnext (false){
if(t>1){
//We build a next object.
hasnext = true;
//Here we use placement new to build the object in the next allocated memory
// No need to store a pointer.
//We know that this is in the next contiguous location (see memory allocation below)
new (this+1) MyRawObject(t-1);
}
}
void print(){
Rprintf(" => %d ", type);
if(this->hasnext){
//Next object located is in the next contiguous memory block
(this+1)->print(); //Next in allocated memory
}
}
};
extern "C" {
SEXP CreateRawObject(SEXP type) {
SEXP ans;
int nb = length(type);
PROTECT(ans=allocVector(VECSXP, nb));
int * typeP = INTEGER(type);
//When allocating memory, we need to know the size of our object.
int MOsize =sizeof(MyRawObject);
for(int i=0; i<nb; i++){
SEXP rawData;
//Allocate the memory using R RAW data type.
//Take care to allocate the right amount of memory
//Here we have typeP[i] objects to create.
PROTECT(rawData=allocVector(RAWSXP, typeP[i]*MOsize));
//Memory address of the allocated memory
unsigned char * buf = RAW(rawData);
//Here we use placement new to build the object in the allocated memory
new (buf) MyRawObject(typeP[i]);
SET_VECTOR_ELT(ans, i, rawData);//Put in vector
UNPROTECT(1);
}
UNPROTECT(1);
return ans;
}
SEXP printRawObject(SEXP myObjects){
int nb = length(myObjects);
for(int i=0; i<nb; i++){
SEXP moS=VECTOR_ELT(myObjects, i);
//Use reinterpret_cast to have a pointer of type MyRawObject pointing to the RAW data
MyRawObject * mo= reinterpret_cast<MyRawObject *>(RAW(moS));
if(mo!=NULL){
Rprintf("Address: %d", mo);
mo->print();
Rprintf("\n");
}else{
Rprintf("Null pointer!\n");
}
}
return R_NilValue;
}
}
这些函数可以直接在 R 中使用,例如:
## Create the internal data structure
xx <- .Call("CreateRawObject", 1:10)
##Print
.Call("printRawObject", xx)
## Save it
save(xx, file="xxRaw.RData")
## remove all objects
rm(xx)
## Load the data
load(file="xxRaw.RData")
##Works !
.Call("printRawObject", xx)
这个解决方案有几个问题:
- 不调用析构函数(取决于您在析构函数中所做的事情,这可能是个问题)。
- 如果您的类包含指向其他对象的指针,则所有指针都应相对于内存位置,以便在重新加载后使其工作(就像这里的链表一样)。
- 这是相当复杂的。
- 我不确定保存的文件 (.RData) 是否是跨平台的(跨计算机共享 RData 文件是否安全?),因为我不确定用于存储对象的内存是否相同所有平台。
第二种解决方案:将对象存储为 R 和 C++ 结构,并在重新加载后重建 C++ 对象。
这个想法是在每个函数调用中检查 C++ 指针是否正确。如果不是,则重建对象,否则忽略该步骤。这之所以成为可能,是因为我们直接修改(在 C++ 中)R 对象。因此,更改将对所有后续调用有效。优点是 C++ 对象只构建一次(不是针对每个函数调用),但数据存储两次。这具有在大型项目中更容易实施的重大优势。
在文件“myobject.cpp”中。
#include <Rinternals.h>
//Our object can be made simpler because we can use pointers
class MyObject{
int type;
//Pointer to the next object
MyObject *next;
public:
MyObject(int t):type(t), next(NULL){
if(t>1){
next = new MyObject(t-1);
}
}
~MyObject(){
if(this->next!=NULL){
delete next;
}
}
void print(){
Rprintf(" => %d ", type);
if(this->next!=NULL){
this->next->print();
}
}
};
void finalizeMyObject(SEXP ptr){
MyObject * mo= static_cast<MyObject *>(R_ExternalPtrAddr(ptr));
delete mo;
}
extern "C" {
SEXP CreateObject(SEXP type) {
SEXP ans;
int nb = length(type);
PROTECT(ans=allocVector(VECSXP, nb));
int * typeP = INTEGER(type);
SEXP cl;
PROTECT(cl = allocVector(STRSXP, 1));
SET_STRING_ELT(cl, 0, mkChar("myObject"));
for(int i=0; i<nb; i++){
MyObject * mo = new MyObject(typeP[i]);
SEXP tmpObj = R_MakeExternalPtr(mo, R_NilValue, R_NilValue);
R_RegisterCFinalizerEx(tmpObj, (R_CFinalizer_t) finalizeMyObject, TRUE);
classgets(tmpObj, cl);
SET_VECTOR_ELT(ans, i, tmpObj);//Put in vector
}
UNPROTECT(2);
return ans;
}
SEXP printMyObject(SEXP myObjects){
int nb = length(myObjects);
for(int i=0; i<nb; i++){
SEXP moS=VECTOR_ELT(myObjects, i);
MyObject * mo= static_cast<MyObject *>(R_ExternalPtrAddr(moS));
if(mo!=NULL){
Rprintf("Address: %d", mo);
mo->print();
Rprintf("\n");
}else{
Rprintf("Null pointer!\n");
}
}
return R_NilValue;
}
//This function check if a C++ object is NULL, if it is, it rebuilds all the C++ objects.
SEXP checkRebuildMyObject(SEXP myObjects, SEXP type){
int nb = length(myObjects);
if(nb==0){
return R_NilValue;
}
if(R_ExternalPtrAddr(VECTOR_ELT(myObjects, 1))){ //Non corrupted ptrs
return R_NilValue;
}
int * typeP = INTEGER(type);
SEXP cl;
PROTECT(cl = allocVector(STRSXP, 1));
SET_STRING_ELT(cl, 0, mkChar("myObject"));
for(int i=0; i<nb; i++){
MyObject * mo = new MyObject(typeP[i]);
SEXP tmpObj = R_MakeExternalPtr(mo, R_NilValue, R_NilValue);
R_RegisterCFinalizerEx(tmpObj, (R_CFinalizer_t) finalizeMyObject, TRUE);
classgets(tmpObj, cl);
SET_VECTOR_ELT(myObjects, i, tmpObj);//Put in vector
}
UNPROTECT(1);
return R_NilValue;
}
}
在 R 端,我们可以使用如下函数:
dyn.load("myobject.dll")
CreateObjectR <- function(type){
## We use a custom object type, that store both C++ and R data
type <- as.integer(type)
mo <- list(type=type, myObject= .Call("CreateObject", type))
class(mo) <- "myObjectList"
return(mo)
}
print.myObjectList <- function(x, ...){
## Be sure to check the C++ objects before calling the functions.
.Call("checkRebuildMyObject", x$myObject, x$type)
.Call("printMyObject", x$myObject)
invisible()
}
xx <- CreateObjectR(1:10)
print(xx)
save(xx, file="xx.RData")
rm(xx)
load(file="xx.RData")
print(xx)