1. C++调用R

有时为了调试C++程序,必须在C++环境中模拟R的一些功能,如传输数据到算法,返回结果到R,以及调用R中已有的一些功能函数。这时需要使用到的第三方类库主要有RInside, Rcpp以及R提供的头文件。

1.1 类库准备

1.1.1 R头文件

若需要调用R所提供的功能以及与R交互,那么需要找到R所提供的接口文件“R.h, Rmath.h”,以及库文件”libR.so”。

$sudo find / -name "Rmath.h" | grep "usr/"
/usr/share/R/include/Rmath.h

$sudo find / -name "*R.so" | grep "usr/"
/usr/lib/R/lib/libR.so

1.1.2 RInside安装

RInside的安装比较容易。这是一个R的C++扩展包,只需要在R的环境中使用一般的包安装流程就可以实现。现在给出ubuntu环境下的安装步骤,不加说明,默认操作系统为ubuntu。

$sudo R
>install.packages("RInside")

要想使用这个类库,得首先找到类库接口所提供的头文件如:”RInside.h”。在操作系统中,打开shell,使用以下命令搜索文件所在位置。

$sudo find / -name "*RInside.h" | grep "usr/"
/usr/local/lib/R/site-library/RInside/include/RInside.h

以及库所在的路径,这些对后面C++编译环境特别重要。

$sudo find / -name "*RInside.so" | grep "usr/"
/usr/local/lib/R/site-library/RInside/lib/libRInside.so
/usr/local/lib/R/site-library/RInside/libs/RInside.so

1.1.3 Rcpp安装

Rcpp的安装与RInside过程类似,在R环境中使用一般的包安装策略就可以实现。

$sudo R
>install.packages("Rcpp")

$sudo find / -name "Rcpp.h" | grep "usr/"
/usr/local/lib/R/site-library/Rcpp/include/Rcpp.h

$sudo find / -name "*Rcpp.so" | grep "usr/"
/usr/local/lib/R/site-library/Rcpp/libs/Rcpp.so

$cd /usr/local/lib/R/site-library/Rcpp/libs
$sudo cp Rcpp.so libRcpp.so

C++默认编译命令“-lRcpp”所寻找的链接库为”libRcpp.so”,所以需要对Rcpp.so 重命名。

1.2 环境搭建

这里主要是在ubntu环境下,用Codeblocks来搭建实验环境。首先新建一个空白工程。然后在[project]->[build options]选择[Search directories]->[Compiler]->[Add],并添加以下搜索路径:

/usr/share/R/include
/usr/local/lib/R/site-library/RInside/include
/usr/local/lib/R/site-library/Rcpp/include

以及[Linker]选项卡,添加以下类库搜索路径:

/usr/local/lib/R/site-library/RInside/lib/
/usr/local/lib/R/site-library/Rcpp/libs

[Linker settings]选项卡中[Link libraries]添加以下编译命令:

R
RInside
Rcpp

至此,所需要的环境已基本建立好了。下面以clusterpathRcpp类库为例来具体分析R与C++之间的交互。 clusterpathRcpp可以在http://R-Forge.R-project.org这里下载。

1.3 clusterpathRcpp源代码分析

在clusterpathRcpp的类库目录src下把所有的C++文件加入到codeblocks当前工程,同时新建interface.cpp的头文件“interface.h”。因为cpp文件多次include的情况下,每次编译会出现重复定义的情况。

#ifndef INTERFACE_H_INCLUDED
#define INTERFACE_H_INCLUDED

#include <Rcpp.h>
#include "l1.h"
#include "l2.h"

SEXP tree2list(Cluster *c);

RcppExport SEXP join_clusters_convert(SEXP xR);

SEXP res2list(Results *r);

RcppExport SEXP calcW_convert(SEXP x_R,SEXP gamma);

RcppExport SEXP join_clusters2_restart_convert
(SEXP x_R,SEXP w_R,
SEXP lambda,SEXP join_thresh,SEXP opt_thresh,SEXP lambda_factor,SEXP smooth,
SEXP maxit,SEXP linesearch_freq,SEXP linesearch_points,SEXP check_splits,
SEXP target_cluster,
SEXP verbose
);

#endif // INTERFACE_H_INCLUDED

接下来,就是最重要的测试部分,新建一个带main函数的cpp文件,

#include <iostream>
#include "interface.h"

#include <Rcpp.h>
#include <RInside.h>

using namespace std;

int main(int argc, char* argv[])
{
//example 1: cpp code directly invoke R
RInside R(argc,argv);
R["txt"]="Hello world in R!\n"; //将字符串赋于R环境中的txt对象。
R.parseEvalQ("cat(txt)");       //R环境分析与执行“cat(txt)"命令
cout << "Hello world in C++!" << endl; //C++环境输出

//使用Rcpp类库提供的方法,将C++对象赋于R环境中的”vec1"对象。
R["vec1"]= Rcpp::NumericVector::create(1,2.3,4,5.7,8.0,9);
R.parseEvalQ("print(vec1)");    //R环境分析与执行,并将结果打印到console

std::string txt =
"z<-rnorm(n = 10,mean = 0,sd = 1)";

//把字符串中的内容发送到R环境,并将计算结果赋于C++对象 v
Rcpp::NumericVector v = R.parseEval(txt); 

for (int i=0; i< v.size(); i++) {           //显示c++对象v的内容
std::cout << "In C++ element " << i << " is " << v[i] << std::endl;
}


//example 2: 调试clusterpathRcpp类库, 
//具体的业务逻辑见clusterpathRcpp类库的R子目录中的函数定义
std::string txt2 =
"suppressMessages(library(clusterpathRcpp))";
R.parseEvalQ(txt2);
Rcpp::NumericVector dt((SEXP) R.parseEval("dt<-iris[,1]"));
R["res1"]=join_clusters_convert(dt);  //SEXP类型可以和R中的对象互转

std::string txt3=
"cat('\nres1='); print(res1)";
R.parseEvalQ(txt3);

return 0;
}
  • 一个重要结论:Rcpp提供了两个关键函数用于R与C++之间的数据通信。
    • Rcpp::as(): 将R中的对象转化为C++对象
    • Rcpp::wrap(): 将C++对象转化为R对象

1.4 RInside, Rcpp, R代码分析

1.4.1 代码分析1

下面将系统的分析一下C++与R的交互代码。

int main(int argc, char* argv[]){
    RInside R(argc,argv); //实例化R交互对象。
    std::string txt4 =
        "suppressMessages(library(fPortfolio));"
        "lppData <- 100 * LPP2005.RET[, 1:6];"
        "ewSpec <- portfolioSpec();"
        "nAssets <- ncol(lppData);";
    R.parseEvalQ(txt4);

    const double dvec[6] = {0.1,0.1,0.1,0.1,0.3,0.3};
    const std::vector<double> w(dvec, &dvec[6]);
    R["weightsvec"] = w;
    std::string txt4 = "print(weightsvec);";
    R.parseEvalQ(txt4);
    txt4 = "setWeights(ewSpec)<-weightsvec";
    R.parseEvalQ(txt4);

    txt4 =
        "ewPf <- feasiblePortfolio(data=lppData, spec=ewSpec, constraints=\"LongOnly\");"
        "print(ewPf); "
        "vec <- getCovRiskBudgets(ewPf@portfolio);";
    Rcpp::NumericVector V( (SEXP)R.parseEval(txt4));
    Rcpp::CharacterVector names((SEXP)R.parseEval("names(vec)"));

    std::cout<<"\n\n And now from C++\n\n";
    for(int i=0; i<names.size();i++){
        std::cout<<std::setw(16)<<names[i]<<"\t"
                << std::setw(11)<<V[i]<<"\n";
    }
 }

通过上面的例子可以看出,R与C++的交互主要通过以下两种方式。

  • 发送命令至R
    • 字符串
    • C++对象赋于R代理对象
         std::string txt4= "print(weightsvec)"
         R.parseEvalQ(txt4);
      

      这是C++程序中,通过文本串的形式,将所要的操作发送至R执行环境。

	R["weightsvec"] = w;

这里应该是先将C++对象传递给RInside对象,然后RInside对象再(wrap)传递给R代理对象。

  • 接收R执行结果
    Rcpp::NumericVector V( (SEXP)R.parseEval(txt4));
    

    然后就可以在C++环境中,对Rcpp对象进行处理。

1.4.2 代码分析2(Rcpp中的as用法)

int main(int argc, char* argv[])
{
try{
        RInside R(argc, argv);
        std::string txt;
        txt = "m<-1.23";
        double d1 = Rcpp::as<double>(R.parseEval(txt));
        std::cout<< "d1:\t"<<d1<<std::endl;

        txt = "M <- 1.0*1:6";
        std::vector<double> d2 = Rcpp::as<std::vector<double> >(R.parseEval(txt));
        std::cout<<"d2[0]:\t" <<d2[0]<<"\nd2[1]:\t"<<d2[1]<<std::endl;


        std::string txt2 =
            "x <- rnorm(10,0,1)";
        std::vector<double> d3 = Rcpp::as<std::vector<double> >(R.parseEval(txt2));
        cout<<"begin to print the result that received from R\n";
        for(vector<double>::iterator iter=d3.begin(); iter<d3.end(); iter++){
            cout<<*iter<<"\t";
        }
        cout<<"\n";

        R["x"]=10;
        R["y"]=20;
        R.parseEval("z<-x+y");
        int sum = R["z"];
        std::cout<<"10+20="<<sum<<std::endl;

        sum=R.parseEval("x+y");   //直接隐式转换,把R对象转化为C++对象。
        std::cout<<"10+20="<<sum<<std::endl;

    }catch(std::exception ex){
        std::cerr<<"Exception caught:"<<ex.what()<<std::endl;
    }catch(...){
        std::cerr<<"Unknown exception caught"<<std::endl;
    }
  • Rcpp类库提供的as函数可以把R环境得到的SEXP对象转化为C++对象(基本对象如int, double等以及stl对象如vector,map等)。另外,R[“var_name”]可以直接存取R环境中的对象,也可以当作右值赋于一个C++对象,此时发生as的隐式转换。

1.4.3 C++函数输出到R环境

std::string hello( std::string who ){
    std::string result( "hello " ) ;
    result += who ;
    return result;
} 

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

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

    // expose the "hello" function in the global environment
    R["hello"] = Rcpp::InternalFunction( &hello ) ;
   
    // call it and display the result
    std::string result = R.parseEvalNT("hello(\"world\")") ;
    std::cout << "hello( 'world') =  " << result << std::endl; 

    exit(0);
}

2. R扩展——Rcpp包开发

R开发虽然很容易,但涉及到一些底层操作,如循环的时候,需要用C++的内存操作来加速。 此外, C/C++已有一些开发好的库,没必要用R重写,可以为R所调用。

2.1 .C调用

这种方式使用C语言由R调用,主要有以下几个步骤:

  • C语言编写的函数
  • 编译
  • 加载
  • 调用

下面,给出一个简单的示例

2.1.1 C语言写的函数

void double_me(int* x)
{
	*x = *x+*x;	
}

2.1.2 编译

$R CMD SHLIB doubler.c

2.1.3 加载

dyn.load("doubler.so")

2.1.4 调用

.C("double", x=as.integer(4))

这种方式虽然简单易懂,但R提供的语言接口过于复杂,容易出错,不好管理。

2.2 使用Rcpp包服务扩展R包

Rcpp程序包为用R调用C++代码提供了便利。使用C++来扩展R包,Rcpp及RInside提供了无缝的衔接。

2.2.1 使用Rcpp构建扩展包框架

在R环境中,可以可使用Rcpp包提供的函数(Rcpp.package.skeleton)得到扩展包的框架。

>libirary(Rcpp)
>Rcpp.package.skeleton( "Project_Name" )

在生成的R包 skeleton中, 可以找到rcpp_hello_world.cpp文件, 可以尝试添加几个函数, 修改成如下样式 (注意以下代码为C++文件,扩展名为cpp)

#include <Rcpp.h>
using namespace Rcpp;
using namespace std;
// 注意 #include <Rcpp.h> 是调用Rcpp的必要条件
RcppExport SEXP intVec1a(SEXP vec) {
   Rcpp::NumericVector prod_vec(vec);
   int prod = 1;
   for (int i=0; i<vec2.size(); i++) {
       prod *= vec2[i];
   }
   return (wrap(prod));
}

RcppExport SEXP square2(SEXP s)
{ 
 double s_c = Rcpp::as<double>(s);
 double squared = s_c*s_c;
 return (wrap(squared));
}

RcppExport SEXP expfun(SEXP x){
double x2 = Rcpp::as<double>(x);
double ans=1.0, term=1.0, eps=1e-16;
int n=0;
while (fabs(term)>eps){
   n++;
   term = term * x2 / n;
   ans = ans + term;
}
  return(wrap(ans));
}

2.2.2 编译与安装自定义包

编译:将R的工作目录设定为当前包所在的目录,在R环境中输入,

>compileAttributes()

在windows console或linux bash环境下,输入,

$R CMD INSTALL Project_Name

2.2.3 调用

进入R环境,

>library(Project_Name)
>.Call('square2', s= 520)
>.Call('prod_vec', vec= c(3, 4, 5))
>.Call('expfun', x=3)

2.2.4 卸载

进入R环境,

>remove.packages("package name")

2.2.5 Wrap

每次调用都需要调用.Call,不是很方便,可以用R函数包装一下。

expfunR <- function(xx){
   return( .Call('expfun', x= xx))
}

square2R <- function(ss){
   return( .Call('square2', s= ss))
}

intVecR <- function(x){
   return(.Call('prod_vec', vec= x))
}