从C++调用R函数

45

我希望在我的编译的C++代码中,检查R中是否加载了一个库包(如果没有,则加载它),从该库中调用一个函数,并将结果返回到我的C++代码中。

有人可以指导我吗?似乎有很多关于R和不同的调用方式的信息,但是我没有找到我想做的事情的确切方法。

谢谢。


1
你能详细说明一下plethora是什么吗?有编写R扩展手册中所描述的,还有像Rcpp/RInside这样的扩展。你还遇到了什么,为什么它们不符合要求? - Dirk Eddelbuettel
2个回答

56

Dirk提出了RInside可以让生活更轻松,但对于一些顽固分子...本质上来自于Writing R Extensions第8.1和8.2节,以及R附带的示例。下面的材料涵盖了构建和评估调用;处理返回值是不同(在某种意义上更容易)的话题。

安装设置

假设使用Linux/Mac平台。首先,R必须已被编译为允许链接到共享或静态R库。我使用R源代码的svn副本,在目录~/src/R-devel中工作。我切换到其他目录,称之为~/bin/R-devel,然后

~/src/R-devel/configure --enable-R-shlib
make -j

这将生成~/bin/R-devel/lib/libR.so;也许你正在使用的发行版已经具备此功能?-j标志可以并行运行make命令,从而大大加快构建速度。

有关嵌入示例,请参考~/src/R-devel/tests/Embedding中的示例,可使用cd ~/bin/R-devel/tests/Embedding && make进行编译。显然,这些示例的源代码非常有教育意义。

代码

为了说明,请创建一个名为embed.cpp的文件。首先包括定义R数据结构和R嵌入接口的头文件;它们位于bin/R-devel/include中,并作为主要文档。我们还有一个函数原型,用于执行所有工作。

#include <Rembedded.h>
#include <Rdefines.h>

static void doSplinesExample();

工作流程是启动R,执行工作,然后结束R:

int
main(int argc, char *argv[])
{
    Rf_initEmbeddedR(argc, argv);
    doSplinesExample();
    Rf_endEmbeddedR(0);
    return 0;
}

嵌入下的示例包括调用library(splines)、设置命名选项,然后运行函数example("ns")。以下是执行此操作的例程。

static void
doSplinesExample()
{
    SEXP e, result;
    int errorOccurred;

    // create and evaluate 'library(splines)'
    PROTECT(e = lang2(install("library"), mkString("splines")));
    R_tryEval(e, R_GlobalEnv, &errorOccurred);
    if (errorOccurred) {
        // handle error
    }
    UNPROTECT(1);

    // 'options(FALSE)' ...
    PROTECT(e = lang2(install("options"), ScalarLogical(0)));
    // ... modified to 'options(example.ask=FALSE)' (this is obscure)
    SET_TAG(CDR(e), install("example.ask"));
    R_tryEval(e, R_GlobalEnv, NULL);
    UNPROTECT(1);

    // 'example("ns")'
    PROTECT(e = lang2(install("example"), mkString("ns")));
    R_tryEval(e, R_GlobalEnv, &errorOccurred);
    UNPROTECT(1);
}

编译和运行

现在我们已经准备好将所有东西组合起来了。编译器需要知道头文件和库的位置。

g++ -I/home/user/bin/R-devel/include -L/home/user/bin/R-devel/lib -lR embed.cpp

编译后的应用程序需要在正确的环境中运行,例如正确设置 R_HOME;这可以很容易地安排(显然部署的应用程序希望采取更广泛的方法):

R CMD ./a.out

根据您的目标,写作R扩展的第8节的某些部分可能并不相关,例如,回调函数需要实现R之上的GUI,但不需要评估简单代码块。

一些细节

稍微详细地讲解一下... SEXP(S表达式)是R表示基本类型(整数、逻辑、语言调用等)的数据结构。这行代码

    PROTECT(e = lang2(install("library"), mkString("splines")));

生成一个符号库 library 和一个字符串 "splines",并将它们置于由两个元素构成的语言结构中。这将构建一个未评估的语言对象,大致等同于 R 中的 quote(library("splines"))lang2 返回一个已从 R 的内存池中分配的 SEXP,并且需要使用 PROTECT 保护免受垃圾收集。当内存不再需要保护时,PROTECT 将指向的地址添加到保护栈中,在弹出地址后,内存可用于其他用途(在几行下面用 UNPROTECT(1) 弹出)。该行

    R_tryEval(e, R_GlobalEnv, &errorOccurred);

尝试在R的全局环境中评估e。如果发生错误,则将errorOccurred设置为非0。 R_tryEval返回表示函数结果的SEXP,但在此我们忽略它。因为我们不再需要为library("splines")分配内存,所以我们告诉R它不再受到PROTECT保护。

下一个代码块类似,评估options(example.ask = FALSE),但是调用的构造更加复杂。lang2创建的S表达式是一组对列表,概念上具有节点、左指针(CAR)和右指针(CDR)。 e的左指针指向符号optionse的右指针指向对列表中的另一个节点,其左指针为FALSE(右指针为R_NilValue,表示语言表达式的结束)。 对列表的每个节点都可以有一个TAG,其含义取决于节点所起的作用。 在这里,我们附加参数名称。

    SET_TAG(CDR(e), install("example.ask"));
下一行代码会评估我们构造的表达式 (options(example.ask=FALSE)),使用 NULL 表示忽略函数评估的成功或失败。另一种构造和评估调用的方法在 R-devel/tests/Embedding/RParseEval.c 中展示,这里做了一些修改。
PROTECT(tmp = mkString("options(example.ask=FALSE)"));
PROTECT(e = R_ParseVector(tmp, 1, &status, R_NilValue));
R_tryEval(VECTOR_ELT(e, 0), R_GlobalEnv, NULL);
UNPROTECT(2);

但这似乎不是一个好的策略,因为它混合了R和C代码,并且不允许在R函数中使用计算参数。相反,应该在R中编写和管理R代码(例如创建一个带有执行复杂的R操作的函数的包),由您的C代码使用。

以上代码块的最后一个部分构造并评估example("ns")Rf_tryEval返回函数调用的结果,因此

SEXP result;
PROTECT(result = Rf_tryEval(e, R_GlobalEnv, &errorOccurred));
// ...
UNPROTECT(1);

将其捕获以供后续处理。


41

有一个叫做Rcpp的工具可以让你轻松地用C++扩展R,并且可以让C++代码回调到R。软件包中包含了一些示例。

但是也许你真正想要的是保留你自己的C++程序(即你自己的main()),并调用R?这可以通过RInside最容易实现,它可以非常轻松地将R嵌入到你的C++应用程序中,测试库、加载(如果需要)和函数调用变得非常容易,而且其中包括了多达十几个的示例来帮助你。同时Rcpp还可以帮助你在C++和R之间传递结果。

编辑:由于Martin友善地展示了官方方式,我无法避免将其与RInside随附的示例之一进行对比。这是我曾经快速编写的东西,以帮助一个在r-help上询问如何加载(投资组合优化)库并使用它的人。它满足您的要求:加载库,在C++中访问一些数据,将权重向量从C++传递到R,部署R并获取结果。

// -*- mode: C++; c-indent-level: 4; c-basic-offset: 4;  tab-width: 8; -*-
//
// Simple example for the repeated r-devel mails by Abhijit Bera
//
// Copyright (C) 2009         Dirk Eddelbuettel 
// Copyright (C) 2010 - 2011  Dirk Eddelbuettel and Romain Francois

#include <RInside.h>                    // for the embedded R via RInside

int main(int argc, char *argv[]) {

    try {
        RInside R(argc, argv);          // create an embedded R instance 

        std::string txt = "suppressMessages(library(fPortfolio))";
        R.parseEvalQ(txt);              // load library, no return value

        txt = "M <- as.matrix(SWX.RET); print(head(M)); M";
        // assign mat. M to NumericMatrix
        Rcpp::NumericMatrix M = R.parseEval(txt); 

        std::cout << "M has " 
                  << M.nrow() << " rows and " 
                  << M.ncol() << " cols" << std::endl;

        txt = "colnames(M)";        // assign columns names of M to ans and
        // into string vector cnames
        Rcpp::CharacterVector cnames = R.parseEval(txt);   

        for (int i=0; i<M.ncol(); i++) {
            std::cout << "Column " << cnames[i] 
                      << " in row 42 has " << M(42,i) << std::endl;
        }

    } catch(std::exception& ex) {
        std::cerr << "Exception caught: " << ex.what() << std::endl;
    } catch(...) {
        std::cerr << "Unknown exception caught" << std::endl;
    }

    exit(0);
}

这里有一个名为rinside_sample2.cpp的文件,而且包里面还有很多其他的例子。要构建它,只需要输入命令'make rinside_sample2',因为提供的Makefile已经配置好了找到R、Rcpp和RInside。


4
我原本以为Rcpp也会嵌入(embedding)部分。因为你的回答明显更好,所以我删除了我的回答。 - David Heffernan
2
Rcpp让你可以访问函数等内容,但如果你是从一个独立的C++程序来的,你实际上并没有一个R进程。R嵌入API提供了这个功能,而RInside通过抽象掉很多细节使其更易于使用。 - Dirk Eddelbuettel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接