我们的第三个工程,会秀一下Rust语言的其中一个最棒的优点:没有实际上的运行时环境.
随着组织的增加,他们依赖于等多的编程语言.不同的编程语言有各自的有点和缺点,一个全语言栈的可以使你使用一种语言的优点的同时,用另一种语言来代替它的缺点.
许多编程语言的一个通病就是运行时环境下的性能很差.通常来说,使用一种慢速的语言,代价换来的是生产力的提高.为了缓和这个问题,有一种方法是使用C语言来编写你的系统,然后调用这些C语言代码就像它是由高层次语言写的.这被称为"外部函数接口(foreign function interface)",缩写为FFI.
Rust在两个方向上都支持FFI:它可以方便的调用C语言,并且更为关键的是,他可以像C语言一样被方便的调用.Rust语言没有垃圾回收机制,同时运行时消耗又很小,这使得Rust成为了被嵌入其他语言中的完美候选人.
本章专门讲述FFI,它的一些特性全书中也都有描述,但是在这里,我们使用3个例子来说明FFI特性:Ruby,Python和JavaScript.
3.3.1 问题
我们本来有很多可以选择的例子,但是我们选择了一个Rust语言相较于其他语言的一个明显的优势领域:数值计算和线程.
Page 56
许多编程语言为了一致性的考虑,将数值存放在堆上,而不是栈上.尤其是那些面向对象的语言并且使用垃圾回收的语言,堆是他们的默认选择.有时候会有优化,将特定的数值放在栈上,但是这是基于一个优化器来完成的,那就是我们需要保证我们使用的是基本数值类型而不是对象类型.
第二,许多语言都有一个"全局解释器锁(global interpreter lock)",这会限制多线程的运行.好的方面看这样更安全,但是坏的方面就是本来可以多线程的任务无法多线程执行.
为了强调这两个问题,我们来创建一个重度使用这两个问题的小程序.因为我们关注的焦点是将Rust语言嵌入其他语言中,而不是问题本身,我们使用一个玩具例子:
启动10个线程,其中,每个线程从1数到500万.当是个线程结束的时候打印"done!".
我在我的电脑上选择500万.下面是Ruby的例子:
threads = []
10.times do
threads << Thread.new do
count = 0
5_000_000.times do
count += 1
end
end
end
threads.each {|t| t.join }
puts "done!"
试着运行这个例子,然后选择一个数字可以让你的机器跑上几秒钟.这取决于你的硬件,你可以增加这个数字.
在我的机器上,运行这个程序耗时2.156秒.如果我使用了某些进程监控工具,例如top,我可以发现它仅使用了一个cpu核心.这就是GIL在起作用.
这虽然是一个虚拟的程序,但是在真实世界中你可以想象出一个和他类似的问题.我们的目的是,使用切换繁忙线程来模拟并发.
Page 57
3.3.2 一个Rust库
让我们用Rust重写这个问题.首先用Cargo创建一个新的工程:
$ cargo new embed
$ cd embed
这个问题用Rust写很简单:
use std::thread;
#[no_mangle]
pub extern fn process() {
let handles: Vec<_> = (0..10).map(|_| {
thread::spawn(|| {
let mut _x = 0;
for _ in (0..5_000_001) {
_x += 1;
}
})
}).collect();
for h in handles {
h.join().ok().expect("Could not join a thread!");
}
}
部分代码看起来有些眼熟.我们循环切换了10个线程,将它们收集到一个handles的向量中.在每个线程里面,我们循环500万个次,每次给_x变量加1.为什么是下划线?如果去掉就会有这个结果:
Page 58
第一个警告是因为我们在构建一个库.如果我们添加了一个测试方法,这个警告就会消失.但是现在没有测试方法.
第二个和x于_x有关.因为我们并没有真正让x做任何事情,我们得到了这个警告.在此例中,这是正常的,我们就是要浪费CPU循环.在x前面加上下划线就会去掉这个警告.
最后,我们join每一个线程.
现在,我们有了一个Rust库,但是没有任何C语言程序调用它.如果我现在把它接入其他程序中它还不能工作.我们仅需做两点小改动就可以了.第一点是代码的开头部分:
#[no_mangle]
pub extern fn process() {
我们需要增加一个新的attribute, no_mangle.当你创建一个Rust库的时候,它会在编译时改变函数的名字.这么做的原因不在本书的讨论范围内,但是为了使其他语言能够调用我们的库,我们不能改变函数的名字.这个attribute就是关闭这个行为的.
另一个改动是pub extern.pub意味着这个函数将被从本模块外部调用,extern是说它会被C语言调用.就这样!并没有改变很多代码.
第二件事是我们需要修改Cargo.toml文件.在底部增加这个:
[lib]
name = "embed"
crate-type = ["dylib"]
这样就会告诉Rust我们想要将我们的库编译成一个标准动态库.默认的,Rust会编译成一个"rlib",一个rust格式的库.
现在我们编译一下:
我们选择用cargo build --release,这样会有编译优化.我们想尽可能的执行越快越好.你可以在target/release目录找到输出文件.
这个embed.dll就是我们的"共享对象"库.我们可以和使用其他C语言写的共享对象一样使用它!
现在我们已经有了Rust库,让我们在Ruby中使用它.
Page 59
3.3.3 Ruby
打开embed.rb文件,输入下述代码:
require ‘ffi‘
require ‘Time‘
start = Time.now
module Hello
extend FFI::Library
ffi_lib ‘target/release/embed.dll‘
attach_function :process, [], :void
end
Hello.process
diff = Time.now - start
puts diff
puts "done!"
在正常运行前我们需要安装ffi gem:
$gem install ffi
最后,运行这个ruby脚本:
$ ruby embed.rb
0.002
done!
WOW,真快!在我的系统上只用了0.002秒.ruby脚本需要运行2秒多.我们来看一下代码:
require ‘ffi‘
我们需要首先引入ffi的gem包.这可以让我们的Rust代码像C语言的库一样被使用.
Page 60
module Hello
extend FFI::Library
ffi_lib ‘target/release/embed.dll‘
ffi的作者强烈建议使用一个module来包含我们从共享对象引入的代码.其中,我们需要使用FFI::Library模块,然后调用ffi_lib来加载我们的共享对象.我们只需传递路径就可以了,这里是target/release/embed.dll
attach_function :process, [], :void
attach_function方法是FFI包提供的.他的功能是将Rust中的process方法和ruby中的同名方法进行关联.因为prcess方法没有参数,所以第二个参数是空数组,又因为它没有返回值,所以最后一个传入的参数是:void.
Hello.process
这就是真正调用Rust代码的地方.通过调用attach_function和以及之前和Rust模块绑定来实现这一点.这看起来像是一个Ruby方法,但实际上调用了Rust方法!
puts "done!"
最后,和我们之前的需求一样,我们打印"done!".
就是这么简单!两种语言之间的桥梁非常简单的实现了,并且带给我们很多性能提升.
接下来,我们来看看Python!