作為我們的第三個項目,我們決定選擇一些可以展示Rust最強實力的東西:缺少實質的運行時。
當組織增長,他們越來越依賴大量的編程語言。不同的編程語言有不同的能力和弱點,而一個多語言棧讓你使用特定的語言當它的能力起作用,用另一個當它是有弱點的。
一個非常常見的問題是很多編程語言在程序的運行時性能是很差的。經常,使用一個更慢的,不過提供了更強大的程序猿生產力的語言是一個值得的權衡。為了幫助緩和這個問題,它們提供了一個用C編寫部分你的系統,然后接著用高級語言編寫的代碼調用C代碼。這叫做一個“外部語言接口”,通常簡寫為“FFI”。
Rust在所有兩個方向支持FFI:它可以簡單的調用C代碼,而且至關重要的,他也可以簡單的_在_C中被調用。與Rust缺乏垃圾回收和底層運行時要求相結合,這讓Rust成為一個當你需要嵌入其它語言中以提供一些額外的活力時的強大的候選。
這有一個全面[專注于FFI的章節](http://doc.rust-lang.org/stable/book/ffi.html)和詳情位于本書的其它位置。,不過在這一章,我們會檢查這個特定的FFI的用例,通過3個例子,分別在Ruby,Python和JavaScript中。
## 問題
這里有很多不同的項目我們可以選擇,不過我們將選擇一個Rust相比其它語言有明確優勢的例子:數值計算和線程。
很多語言,為了一致性,將數字放在堆上,而不是放在棧上。特別是在專注面向對象編程和使用垃圾回收的語言中,堆分配是默認行為。有時優化會棧分配特定的數字,不過與其依賴優化器做這個工作,我們可能想要確保我們總是使用原始類型而不是使用各種對象類型。
第二,很多語言有一個“全局翻譯鎖”,它在很多情況下限制了并發。這在安全的名義下被使用,也有一定的積極影響,不過它限制了同時可以進行的工作的數量,這是一個很負面的影響。
為了強調這兩方面,我們將創建一個大量使用這兩方面的項目。因為這個例子關注的是將Rust嵌入到其它語言中,而不是問題自身,我們只使用一個玩具例子:
> 啟動10個線程。在每個線程中,從1數到500萬。在所有10個線程結束后,打印“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`,我可以看到它只用了我的機器的一個核。這是GIL在起作用。
雖然這確實是一個虛構的程序,你可以想象許多問題與現實世界中的問題相似。為了我們的目標,啟動一些繁忙的線程來代表一些并行的,昂貴的計算。
## 一個Rust庫
讓我們用Rust重寫這個問題。首先,讓我們用Cargo創建一個新項目:
~~~
$ cargo new embed
$ cd embed
~~~
這個程序在Rust中很好寫:
~~~
use std::thread;
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`加一。為什么用下劃線呢?好吧,如果我們去掉它并編譯:
~~~
$ cargo build
Compiling embed v0.1.0 (file:///home/steve/src/embed)
src/lib.rs:3:1: 16:2 warning: function is never used: `process`, #[warn(dead_code)] on by default
src/lib.rs:3 fn process() {
src/lib.rs:4 let handles: Vec<_> = (0..10).map(|_| {
src/lib.rs:5 thread::spawn(|| {
src/lib.rs:6 let mut x = 0;
src/lib.rs:7 for _ in (0..5_000_001) {
src/lib.rs:8 x += 1
...
src/lib.rs:6:17: 6:22 warning: variable `x` is assigned to, but never used, #[warn(unused_variables)] on by default
src/lib.rs:6 let mut x = 0;
^~~~~
~~~
第一個警告是因為我們正在構建一個庫。如果我們有一個函數的測試函數,就不會有警告了。不過目前它并不會被調用。
第二個與`x`VS`_x`相關。因為實際上我們從未對`x`_進行_任何處理,我們為此得到一個警告。在我們的情況中,這完全木有問題,因為我們只是想浪費CPU循環。對`x`使用下劃線前綴將移除這個警告。
最后,我們同步每個線程。
然而現在,這是一個Rust庫,而且它并沒有暴露任何可以從C中調用的東西。如果現在我們嘗試在別的語言中鏈接這個庫,這并不能工作。我們只需做兩個小的改變來修復這個問題,第一個是修改我們代碼的開頭:
~~~
#[no_mangle]
pub extern fn process() {
~~~
我們必須增加一個新的屬性,`no_mangle`。當你創建了一個Rust庫,它早編譯輸出中改變了函數的名稱。這么做的原因超出了本教程的范圍,不過為了其它語言能夠知道如何調用這些函數,我們需要不這么做。這個屬性將它關閉。
另一個變化是`pub extern`。`pub`意味著這個函數應當從模塊外被調用,而`extern`說它應當能被C調用。這就是了!沒有更多的修改。
我們需要做的第二件事是修改我們的`Cargo.toml`的一個設定。在底部加上這些:
~~~
[lib]
name = "embed"
crate-type = ["dylib"]
~~~
這告訴Rust我們想要將我們的庫編譯為一個標準的動態庫。默認,Rust編譯為一個“rlib”,一個Rust特定的格式。
現在讓我們構建這個項目:
~~~
$ cargo build --release
Compiling embed v0.1.0 (file:///home/steve/src/embed)
~~~
我們選擇了`cargo build --release`,它打開了優化進行構建。我們想讓它越快越好!你可以在`target/release`找到輸出的庫:
~~~
$ ls target/release/
build deps examples libembed.so native
~~~
那個`libembed.so`就是我們的“共享目標(動態)”庫。我們可以像任何用C寫的動態庫使用這個文件!另外,這也可能有`embed.dll`或`libembed.dylib`,基于不同的平臺。
現在我們構建了我們的Rust庫,讓我們在Ruby中使用它。
## Ruby
在我們的項目中打開一個`embed.rb`文件。并這么做:
~~~
require 'ffi'
module Hello
extend FFI::Library
ffi_lib 'target/release/libembed.so'
attach_function :process, [], :void
end
Hello.process
puts "done!”
~~~
在我們可以運行之前,我們需要安裝`ffi`gem:
~~~
$ gem install ffi # this may need sudo
Fetching: ffi-1.9.8.gem (100%)
Building native extensions. This could take a while...
Successfully installed ffi-1.9.8
Parsing documentation for ffi-1.9.8
Installing ri documentation for ffi-1.9.8
Done installing documentation for ffi after 0 seconds
1 gem installed
~~~
最后,我們可以嘗試運行它:
~~~
$ ruby embed.rb
done!
$
~~~
哇哦,這很快欸!在我系統中,它花費了`0.086`秒,而不是純Ruby所需的2秒。讓我們分開我們的Ruby代碼:
~~~
require 'ffi'
~~~
首先我們需要`ffi`gem。這讓我們可以像C庫一樣使用Rust的接口。
~~~
module Hello
extend FFI::Library
ffi_lib 'target/release/libembed.so'
~~~
`ffi`gem的作者建議使用一個模塊來限制我們從共享庫導入的函數的作用域。在其中,我們`extend`必要的`FFI::Library`模塊,接著調用`ffi_lib`加載我們的動態庫。我們僅僅傳遞我們庫存儲的路徑,它是我們之前見過的,是`target/release/libembed.so`。
~~~
attach_function :process, [], :void
~~~
`attach_function`方法由`ffi`gem提供。它用來把我們Rust中`process()`連接到Ruby中同名函數。因為`process()`沒有參數,第二個參數是一個空數組,并且因為它沒有返回值。我傳遞`:void`作為最后的參數。
~~~
Hello.process
~~~
這是實際的Rust調用。我們的`module`和`attach_function`調用的組合設置了環境。它看起來像一個Ruby函數,不過它實際是Rust!
~~~
puts "done!"
~~~
最后,作為我們每個項目的要求,我們打印`done!`。
這就是全部!就像我們看到的,連接兩個語言真是很簡單,并為我們帶來了很多性提升。
接下來,讓我們試試Python!
## Python
在這個目錄中創建一個`embed.py`,并寫入這些:
~~~
from ctypes import cdll
lib = cdll.LoadLibrary("target/release/libembed.so")
lib.process()
print("done!")
~~~
甚至更簡單了!我們使用`ctypes`模塊的`cdll`。之后是一個快速的`LoadLibrary`,然后我可以調用`process()`。
在我的系統,這花費了`0.017`秒。灰常快!
## Node.js
Node并不是一個語言,不過目前它是服務端JavaScript的居統治地位的實現。
為了在Nod中進行FFI,首先我們需要安裝這個庫:
~~~
$ npm install ffi
~~~
安裝之后,我們就可以使用它了:
~~~
var ffi = require('ffi');
var lib = ffi.Library('target/release/libembed', {
'process': [ 'void', [] ]
});
lib.process();
console.log("done!");
~~~
這看起來比Python的例子更像Ruby的例子。我們使用`ffi`模塊來獲取`ffi.Library()`,它加載我們的動態庫。我們需要標明函數的返回值和參數值,它們是返回“void”,和一個空數組表明沒有參數。這樣,我們就可以調用它并打印結果。
在我的系統上,這會花費`0.092`秒,很快。
## 結論
如你所見,基礎操作是_很_簡單的。當然,這里有很多我們可以做的。查看[FFI](http://doc.rust-lang.org/stable/book/ffi.html)章節以了解跟多細節。
- 前言
- 1.介紹
- 2.準備
- 2.1.安裝Rust
- 2.2.Hello, world!
- 2.3.Hello, Cargo!
- 3.學習Rust
- 3.1.猜猜看
- 3.2.哲學家就餐問題
- 3.3.其它語言中的Rust
- 4.高效Rust
- 4.1.棧和堆
- 4.2.測試
- 4.3.條件編譯
- 4.4.文檔
- 4.5.迭代器
- 4.6.并發
- 4.7.錯誤處理
- 4.8.外部語言接口
- 4.9.Borrow 和 AsRef
- 4.10.發布途徑
- 5.語法和語義
- 5.1.變量綁定
- 5.2.函數
- 5.3.原生類型
- 5.4.注釋
- 5.5.If語句
- 5.6.for循環
- 5.7.while循環
- 5.8.所有權
- 5.9.引用和借用
- 5.10.生命周期
- 5.11.可變性
- 5.12.結構體
- 5.13.枚舉
- 5.14.匹配
- 5.15.模式
- 5.16.方法語法
- 5.17.Vectors
- 5.18.字符串
- 5.19.泛型
- 5.20.Traits
- 5.21.Drop
- 5.22.if let
- 5.23.trait對象
- 5.24.閉包
- 5.25.通用函數調用語法
- 5.26.包裝箱和模塊
- 5.27.`const`和`static`
- 5.28.屬性
- 5.29.`type`別名
- 5.30.類型轉換
- 5.31.關聯類型
- 5.32.不定長類型
- 5.33.運算符和重載
- 5.34.`Deref`強制多態
- 5.35.宏
- 5.36.裸指針
- 6.Rust開發版
- 6.1.編譯器插件
- 6.2.內聯匯編
- 6.3.不使用標準庫
- 6.4.固有功能
- 6.5.語言項
- 6.6.鏈接參數
- 6.7.基準測試
- 6.8.裝箱語法和模式
- 6.9.切片模式
- 6.10.關聯常量
- 7.詞匯表
- 8.學院派研究
- 勘誤