1、R语言系统内部函数
R语言的底层函数绝大部分都是由C语言实现的,用户调用这些底层函数时,这些函数会使用
.Primitive、.Internal
函数,调用系统内部函数来实现特定的功能。
使用
.Primitive
调用的内部函数,可以直接与C语言对接,使用形式类似
.Primitive(name)(args)
。这种形式可以直接通过
name
调用C语言中的某个函数,所以运行速度更快。R语言中的操作符、基础数学运算函数、流程控制类函数等,为了提高运行效率,都是以
.Primitive
直接调用内部函数的方式存在的,比如函数
abs
相当于
.Primitive("abs")
。但是这类函数没有函数体、参数列表、定义环境,所以不遵守R语言中的参数查找、函数分发等机制(参考
这里
),只适合简单明确的传参形式。这类函数的数据类型大多为
builtin
,但其中一些函数会将未执行的命令作为参数传递给内部函数,所以数据类型为
special
,比如操作符
&&
在内部函数中才会决定是否计算第二个参数。
使用
.Internal
调用的内部函数,参数可以被R语言处理后,再与C语言对接,使用形式类似
.Internal(call)
。以这种方式作为函数体(或函数体中的一条语句)的函数,本质上与普通函数无异。但需要注意的是,
.Internal
也是用
.Primitive
函数实现的,相当于
.Primitive(".Internal")
。
> .Primitive("+")(1:10, -1)
[1] 0 1 2 3 4 5 6 7 8 9
> .Internal(strtoi("13141F", base = 16L))
[1] 1250335
实际上无论以何种方式调用的内部函数,都以do_作为函数名的前缀,且都只能接受4个SEXP类型的参数,其中op的作用在于,当该内部函数对应R语言中的多个函数时(比如R语言中的操作符<-、<<-、=,都是由C语言中的do_set函数实现的),区分是哪个R语言函数调用了该内部函数;call、args、env分别为R语言中调用该内部函数时的命令、参数列表、执行环境。此外这些内部函数除非抛出异常信息,否则必须且只能返回一个SEXP类型的数据。
R语言的底层代码中,维护了一张R语言函数与C语言函数的对照表—— (R_FunTab,其中规定了哪些函数可以使用.Primitive直接调用,哪些函数需要使用.Internal调用;特定内部函数的op值(即offset);R语言中调用特定内部函数时的参数列表,是需要在R语言中提前执行,还是需要在内部函数中执行……
2、调用用户编写的C/C++程序
除了系统内部函数,R语言也可以调用用户编写的C/C++程序。但用户编写的C/C++程序必须满足一定的规范才可以被R语言调用,否则直接调用可能会导致R语言崩溃。R语言的标准C/C++接口,在安装目录中的include文件夹下,我们可以在R语言中使用命令R.home("include")
找到该文件夹所在路径。
2.1、编写C/C++程序
我们在编写C/C++程序时,一般需要包含Rinternals.h头文件,该文件规定了R语言中的各种数据类型、数据的创建与筛选方式等。同时我们编写的函数参数与返回值必须是SEXP类型的数据(除非我们使用R语言中的.C函数作为C/C++程序的调用接口)。要在SEXP类型的数据与C语言标准类型的数据之间进行转化,我们可以使用Rinternals.h头文件中提供的标准函数。比如以下代码中,我们使用了STRING_ELT函数,截取了字符串数组x中的首个元素;我们还使用了CHAR函数,将CHARSXP类型的数据转化为了C语言中的字符串;此外我们还可以使用allocVector函数创建一个R语言数组;使用SET_VECTOR_ELT函数修改R语言数组中某个元素的值……
#include <Rinternals.h>
SEXP ParseEval(SEXP x){
const char * expr = CHAR(STRING_ELT(x, 0));
SEXP result = R_ParseEvalString(expr, R_GlobalEnv);
return result;
2.2、管理动态链接库
接下来我们需要将编写好的C/C++程序保存在一个文件中(比如ParseEval.c),并在操作系统的命令行下,使用以下命令编译生成动态链接库。注意这里我的标准接口头文件存放在/usr/share/R/include/中。当然我们也可以使用R语言系统编译动态链接库的方法——SHLIB,但这种方法会将汇编过程与链接过程分开执行。
# 使用gcc编译生成动态链接库
gcc -o ParseEval.so ParseEval.c -I . -I /usr/share/R/include/ -shared -fpic
# 使用SHLIB编译生成动态链接库
R CMD SHLIB -o ParseEval.so ParseEval.c
接下来我们需要在R语言中载入、管理编译好的动态链接库文件。我们可以分别使用dyn.load、dyn.unload函数,载入、脱载动态链接库;使用getLoadedDLLs函数查看所有已被载入的动态链接库;使用is.loaded函数查看动态链接库中的某个函数是否已被载入。
2.3、在R语言中调用C/C++程序
理论上,R语言中载入动态链接库后,就可以直接使用.C、.Call、.External等函数,调用动态链接库中的C/C++程序。以上三个函数的区别在于:.C函数会将SEXP类型的R语言参数,转化为C语言中相应类型的数据(详情请参照帮助文档中的转化对照表),且.C函数被调用后返回值即传递的参数,不能从C/C++程序中接收返回值;.Call函数会直接将接收到的参数传递给C/C++程序,并从C/C++程序接收返回值传递给R语言;.External函数会将接收到的所有参数作为一个pairlist类型的参数列表传递给C/C++程序,其它方面与.Call函数一致,所以.External函数可以适用于参数个数不确定的程序(如sum函数)。
> .Call("ParseEval", "1:10 - 1")
[1] 0 1 2 3 4 5 6 7 8 9
如果我们在R语言中直接调用C/C++程序,可能会存在一些问题,比如C/C++程序可能无法处理R语言中的NULL、NA、零长度数组等值,或者我们需要使用R语言的参数处理、函数分发等机制。所以我们可能需要在R语言中编写一个包裹函数,专门用于处理这类不确定因素,并把处理好的参数传递给C/C++程序。
> ParseEval <- function(x){
+ if(!is.character(x) || length(x) == 0)
+ stop("'x'必须是字符串")
+ x <- paste(x, collapse = ";")
+ .Call("ParseEval", x)
2.4、使用第三方程序包调用C/C++程序
通过上述步骤,我们已经可以在R语言中调用C/C++程序了,但以上步骤还可以更加精简。我们可以使用inline程序包中的cfunction、cxxfunction函数,只需写明C/C++程序的参数类型及函数体,即可直接由C/C++程序实现的R语言函数,编译、载入动态链接库、编写包裹函数等过程都可以交由inline程序包完成。
> library(inline, quietly = TRUE)
> ParseEval <- cfunction(
+ sig = c(x = "character"),
+ body = "
+ const char * expr = CHAR(STRING_ELT(x, 0));
+ SEXP result = R_ParseEvalString(expr, R_GlobalEnv);
+ return result;
+ "
同样的,我们也可以使用Rcpp程序包中的cppFunction函数,以更加便捷的方式在R语言中调用C++程序,Rcpp程序包可以自动对参数及返回值的数据类型进行转化,并且直接生成与C++程序同名的R语言函数,所以只需要我们专注于C++程序的编写。
> library(Rcpp, quietly = TRUE)
> cppFunction(
+ code = "
+ SEXP ParseEval(const char * expr){
+ return R_ParseEvalString(expr, R_GlobalEnv);
+ }
+ "
3、调用操作系统命令行命令
我们可以在R语言中使用system、system2函数,调用操作系统命令行中的命令。这两个函数都可以重定向操作系统命令行的标准输入、标准输出、错误输出流,并决定是否在R语言后台调用这些命令,但它们的参数设置略有不同。以下是system2函数的部分参数释义:
command参数即需要被调用的操作系统命令行命令
args参数即需要传递给命令的参数
stdout、stderr参数即操作系统命令行标准输出与错误输出的输出位置,默认输出到R语言命令行,NULL或FALSE表示舍弃这些输出内容,TRUE表示将这些输出内容存放到R语言中的字符串数组对象中,当然也可以是一个路径,表示输出到到特定文件中
stdin参数即操作系统命令行标准输入的接收位置,仅当input参数为空时有效,默认从R语言命令行接收输入,当然也可以是一个路径,表示从特定文件中接收输入
input参数即需要作为操作系统命令行标准输入的字符串数组,其中每个字符串代表一行命令
env参数即需要提前设置的操作系统环境变量,需要以name=value
形式的字符串数组提供
wait参数即R语言是否应该等待命令完成,当需要接收命令的输出内容时,必须等待命令完成
timeout参数即命令完成的时限(秒),如果命令超出这个时限仍未完成,则会终止命令并抛出超时错误
使用system、system2函数,我们不仅可以运行操作系统命令(如ls、cd等),还可以调用其它脚本语言(如python、R等),但这种调用方式并不能直接将命令返回值转化为相应的R语言对象,而且直接将输出结果转化为字符串,往往会破坏输出结果的数据结构。所以这种方式更适合调用操作系统命令行,完成一些不需要用到返回值的操作,比如编译C/C++脚本、后台运行某项任务等。当然我们也可以借助字符串类型的数据交换格式,在操作系统命令行与R语言命令行之间传递数据。比如我们可以将输出结果转化为json字符串,然后在R语言中解析json字符串,即可将输出结果转化为相应的R语言对象。
# 调用操作系统命令行下的R语言
> system("R --slave -e '1:10 - 1'", intern = TRUE)
[1] " [1] 0 1 2 3 4 5 6 7 8 9"
> system2("R", "--slave -e '1:10 - 1'", stdout = TRUE)
[1] " [1] 0 1 2 3 4 5 6 7 8 9"
# 使用json字符串传递高级数据类型的数据
> result <- system2("R", "--slave", stdout = TRUE,
+ input = c("res <- 1:10 - 1", "cat(rjson::toJSON(res))"))
> rjson::fromJSON(result)
[1] 0 1 2 3 4 5 6 7 8 9
4、调用其它计算机语言
上文我们已经介绍了如何在R语言中通过操作系统命令行调用其它计算机语言,但这种方法必须使用操作系统标注输出,才能将其它计算机语言的计算结果(返回值)传递给R语言。当然我们也可以使用第三方程序包,直接在R语言中调用特定的计算机语言,比如我们可以使用rJava程序包,在R与java之间进行数据传递、方法调用。更多关于rava的使用帮助,请参考 这里。
> library(rJava)
# 启动JVM
> .jinit()
[1] 0
# 在JVM中创建一个字符串对象s
> s <- .jnew("java/lang/String", "Hello World!")
# 直接打印字符串对象s
> print(s)
[1] "Java-Object{Hello World!}"
# 获取JVM中字符串对象s对应的值
> .jstrVal(s)
[1] "Hello World!"
# 调用字符串对象s的length方法,
# 注意returnSig参数为返回值对应R语言中的数据类型
# 此外返回值还可以是以下类型:
# I integer D double (numeric) J long (*) F float (*)
# V void Z boolean C char (integer) B byte (raw)
# L<class> <class>类型的Java对象 (比如,Ljava/lang/Object)
# [<type> <type>类型的数组对象 (比如,[D double类型的数组)
> .jcall(s, returnSig = "I", method = "length")
[1] 12
# 调用字符串对象s的indexOf方法
> s$indexOf("World")
[1] 6
并非所有桥接计算机语言的程序包都能实现两种计算机语言之间数据无缝对接,比如rPython程序包就是通过json字符串的形式,在R与pthon之间传递数据的。以下代码展示了rPython在R语言中调用python的基本原理(linux操作系统下调用python2.7),当然rPython中加入了错误处理、跨版本兼容等机制,所以真正实现起来会更加复杂。更多关于python的C/C++接口规范,请参考 这里。
> WD <- tempdir()
> rPython.c <- file.path(WD, "rPython.c")
> rPython.o <- file.path(WD, "rPython.o")
> rPython.so <- file.path(WD, "rPython.so")
# 编写调用python的C语言程序
> writeLines(con = rPython.c, text = '
+ #include <Python.h>
+ void python_init(){
+ Py_Initialize();
+ PyRun_SimpleString("import json");
+ void python_stop(){
+ Py_Finalize();
+ void python_exec(char** code)
+ PyRun_SimpleString(*code);
+ void python_get( const char** name, char** value )
+ PyObject * module = PyImport_AddModule("__main__");
+ PyObject * dictionary = PyModule_GetDict(module);
+ PyObject * result = PyDict_GetItemString(dictionary, *name );
+ *value = PyString_AS_STRING(result);
# 生成rPython动态链接库
> system2("gcc", paste(" -c", rPython.c, " -o", rPython.o,
+ system2("python-config", "--includes", stdout = TRUE)))
> system2("gcc", paste("-shared", rPython.o, " -o", rPython.so,
+ system2("python-config", "--ldflags", stdout = TRUE)))
# 载入rPython动态链接库
> dyn.load(rPython.so)
# 在R语言中编写包裹函数
> python_init <- function(){
+ .C("python_init")
+ invisible(NULL)
> python_stop <- function(){
+ .C("python_stop")
+ invisible(NULL)
> python_exec <- function(code){
+ .C("python_exec", code)
+ invisible(NULL)
> python_get <- function(name){
+ code <- paste( "value_json = json.dumps( [", name, "] )")
+ .C("python_exec", code)
+ value <- .C("python_get", "value_json", value = "")$value
+ rjson::fromJSON(value)
# 在R语言中调用python
> python_init()
> python_exec(" abc = 'Hello World!' ")
> python_get("abc")
[1] "Hello World!"
> python_stop()