Common Lisp 中调用 Rust 代码

未来项目使用该组合的可能?

Posted by David Gu on May 11, 2015

近日在Twitter上看到某君大力推荐一门叫 Rust1 的语言,联想到未来的工作多多少少需要其它系统编程语言的帮助,于是今天抽时间大概了解了下,并尝试着用 Common Lisp 的 CFFI2 库来实现对Rust代码的调用。

Rust is a systems programming language that runs blazingly fast, prevents almost all crashes*, and eliminates data races.

以上是 Rust 语言官方主页上的一小段介绍,似乎有些迷糊。Rust 社区看上去费了不少心思,不仅给出了整个语言的文档(后来发现有未完成的段落),还制做了一段很长很全面的『Tutorial』来介绍这门语言。粗略的来看,Rust 应当是不强迫用户使用某种编程范式的,你既可以用纯函数式的方法来编码,也可以大量的使用赋值。

另外,Rust中有个叫『Traits』的概念,用它可以实现多态性。类似的概念在 Haskell 和 OCaml 中都可以找到,在此不赘述。Rust 还带了个宏(Macro)系统,这倒真的让我眼前一亮;这个系统和 Scheme 的宏系统很类似,于是跟 Common Lisp 的宏就差别很大了。不知道Rust社区的人对这个宏系统是什么看法。

除此以外,Rust 显示出了一门新兴语言应当有的气质。诸如闭包(Closure),类型系统,模式匹配,函数作为第一类对象这样的特性都是(全面或限制性地)支持的。不过,似乎线程安全、并发等一些底层机制才是他的『杀手级』特性;正如他自己所宣称的那样,这是一个系统编程语言,所以他的专注点多多少少肯定也会有些不一样。

如果想了解更多,可以去刚刚提到的『Tutorial』上继续阅读。在这篇博文里,我更关心的是怎样从 Common Lisp 调用Rust 的函数。虽然工作上我主要用 Common Lisp 编写运输层和应用层的代码,但是现在几乎所有的网络程序都是『beta』级别的——总是不定期会被地修改、重新部署与设计。从这个角度看,如果方案中的组件太过于单一,那么有些情况下风险与编码成本会相应地变高。寻找并配置一个若干组件的组合,这样的方案在某些情况下可能会降低一些风险与成本。

Rust 本身是一门搭建在 LLVM 上的编译型语言,这意味着他的编译器应当提供将其代码编译成动态库的功能。于是,我们第一步就应当解决这个问题。例如,如果有以下一小段代码:

#[no_mangle]
pub extern fn add_one(x: i32) -> i32 {
    x + 1
}

#[no_mangle]
pub extern fn factorial(n: i32) -> i32 {
    if n < 2 {
        n
    } else {
        n * factorial(n-1)
    }
}

fn main () {}

函数的功能应该不难理解,一个『+1』函数和一个阶乘函数。值得一提的是,为了能被外部程序调用,函数之前必须要加上一些声明与关键字,详情请阅读这个页面。接下来,我们使用 Rust 编译器 rustc 来将其编译成一个动态库:

$ rustc —crate-type=dylib cffi-test.rs
cffi-test.rs:15:1: 15:14 warning: function is never used: `main`, #[warn(dead_code)] on by default
cffi-test.rs:15 fn main () {}
                ^~~~~~~~~~~~~
$ ls lib*
libcffi_test.dylib

然后我们使用 Common Lisp 的 CFFI 库。CFFI 的全称是『Common Foreign Function Interface』,与其它语言的相关实现所不同的是,CFFI 中的绑定本身就是用 Lisp 写的,而非通过一个诸如 PyObject 那样的中介对象。这一点确实很特殊。下面的代码显示了怎样加载刚刚的『libcffi_test.dylib』。

(defpackage :cffi-test
  (:use :common-lisp :cffi))

(in-package :cffi-test)

(define-foreign-library libcffi-test
  (:darwin "/tmp/libcffi_test.dylib")) ;; 这样写只是为了让这个例子看起来简单一些,实际中应当添加 (:unix "xxx.so") 和 默认值

(use-foreign-library libcffi-test)

(defcfun ("add_one" add-one) :int (x :int))

(defcfun ("factorial" factorial) :int (x :int))

运行结果如下:

; SLIME 2015-05-07
CL-USER> (ql:quickload :cffi)
To load "cffi":
  Load 1 ASDF system:
    cffi
; Loading "cffi"
.........
(:CFFI)
CL-USER> (load "/tmp/cffi-test.lisp")
T
CL-USER> (in-package :cffi-test)
#<PACKAGE "CFFI-TEST">
CFFI-TEST> (add-one 100)
101
CFFI-TEST> (factorial 10)
3628800

所以,至少目前来看这样的组合是可行的。但事实上,到时候所面临的实际问题远不可能这么简单。最近笔者依然在努力完成毕业设计,所以暂时不能继续深入摸索了。以后若有时间,并且真的在项目当中用到了这样的组合,定当撰写后续博文。