我們的第三個項目,我們要選擇展示那些能展示 Rust 最大優(yōu)點的點:大量運行時的減少。
隨著我們組織的發(fā)展,其越來越依賴其他的一些編程語言。不同的編程語言有不同的優(yōu)點和缺點,通曉數(shù)種語言的堆棧允許你使用一個特定的語言,在其的優(yōu)勢方面,而在其弱勢的方面,你可以使用另一種語言。
許多程語言一個共同薄弱的地方就是程序的運行時性能。通常情況下,使用了一種運行比較慢的語言,但是如果它同時能提升程序員的工作效率也是值得的。為了幫助緩解這個問題,他們提供了一個方法,系統(tǒng)中的一部分用 C 來寫,然后再調(diào)用 C 代碼,那么這一部分好像就是用高級語言編寫的似得。這被稱作“外部程序接口”,一般縮寫成“FFI”。
Rust 在兩個方面上支持 FFI:它可以容易的調(diào)用 C 代碼,但至關(guān)重要的是,它也可以像容易調(diào)用 C 代碼那樣被調(diào)用。當你需要一些額外的一些其他功能時,Rust 的無垃圾收集器和較的低運行時需求,這兩點使得 Rust 成為一個嵌入到其他語言中的很好的方案。
在本教程中,我們有一整章來講述 FFI 和它的細節(jié),但是在本章中,我們將用三個例子來展示 FFI 的特定用例,它們分別是在 Ruby,Python 和 JavaScript 中。
這里我們有很多不同的項目可供選擇,但我們要選擇一個能展示 Rust 比其他許多語言有明顯優(yōu)勢的例子:數(shù)值計算和線程。
許多語言為了一致性,將數(shù)字存放在堆上,而不是在堆棧上。尤其是在專注于面向?qū)ο缶幊毯褪褂美占恼Z言上,默認的分配模式是堆分配。有時候優(yōu)化會將特定的數(shù)字分配給堆棧,但它不是依靠優(yōu)化器來完成的這項工作。同時,我們可能希望確保我們使用的總是原始的數(shù)字類型而不是某種形式的對象類型。
第二問題,許多語言有一個“全局解釋器鎖”,這在許多情況下限制了并發(fā)。這是以安全的名義來進行的,本來這是一個好意,但是它限制了同一時間可以完成的工作量,這就非常不好了。
為了強調(diào)這兩個方面,我們要創(chuàng)建一個能使用到這兩方面的一個小項目。因為示例的重點是將 Rust 嵌入到其他語言,而不是這個問題的本身,所以我們只用一個玩具例子:
開十個線程。每個線程內(nèi)部實現(xiàn)從一數(shù)到五百萬。數(shù)完后,十個線程結(jié)束,并打印出“done!”。
這里我基于我計算機的能力選擇了五百萬。下面是一個用 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!"
嘗試運行這個例子,并選擇一個數(shù)字運行幾秒鐘?;谀愕碾娔X硬件,你可能要增大或減小這個數(shù)字。
在我的系統(tǒng)中,運行這個程序需要 2.156 秒。如果我用某種類似 top 的進程監(jiān)控工具,我可以看到它在我的機器上只使用一個 CPU 核。這是由于 GIL 在起作用。
雖然這確實是一個人工合成的程序,但是你也可以想象許多與現(xiàn)實世界相似的問題。就我們的目的而言,運行一些繁忙線程就代表了某種并行,昂貴的計算問題。
讓我們用 Rust 重寫這個問題。首先,讓我們用 Cargo 創(chuàng)建一個新項目:
$ 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!");
}
}
這個程序中的一些內(nèi)容與從先前的例子看起來很相似。我們開啟了十個線程,將它們收集成一個 handles 向量。在每個線程中,我們都循環(huán)五百萬次,每次給 _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;
^~~~~
第一個警告是因為我們正在構(gòu)建一個庫。如果我們有一個關(guān)于此函數(shù)的測試,那么這個警告將會消失。但是現(xiàn)在,這個函數(shù)從未被調(diào)用過。
第二個警告與 x ,_x
有關(guān)。因為我們對 x 沒有做過任何操作,所以我們得到了一個警告。在我們的例子中,這是完全沒有問題的,因為我們就是想浪費 CPU 周期。為 x 添加下劃線前綴就會消除這個警告。
最后,我們連接了每個線程。
然而現(xiàn)在這是一個 Rust 庫,它還沒有公開任何從 C 中可調(diào)用的代碼。如果現(xiàn)在我們試圖將其鏈接到另一種語言,那么它是不能使用的。我們只需要做兩個小改變就能解決這個問題。第一個就是修改我們代碼的開始部分:
#[no_mangle]
pub extern fn process() {
我們必須添加一個新的屬性,no_mangle。當你創(chuàng)建一個 Rust 庫時,在編譯輸出階段會改變函數(shù)的名稱。這個的原因超出了本教程的范圍。為了讓其他語言知道如何調(diào)用函數(shù),我們不需要改變函數(shù)的名稱。這個屬性就是將這個改變功能關(guān)掉。
另一個變化就是 pub extern。pub 意味著在這個模塊以外這個函數(shù)應(yīng)該是可調(diào)用的。extern 表示它應(yīng)該能夠從 C 中被調(diào)用。就這樣了!沒有很多變化了。
我們需要做的第二件事就是是改變 Cargo.toml 的設(shè)置。添加如下的內(nèi)容到底部:
[lib]
name = "embed"
crate-type = ["dylib"]
這會告訴 Rust,我們想將我們的庫編譯成標準動態(tài)庫。默認情況下,Rust 會編譯成一個 ‘rlib’,這是一個 Rust 獨有的格式。
現(xiàn)在讓我們來構(gòu)建項目:
$ cargo build --release
Compiling embed v0.1.0 (file:///home/steve/src/embed)
我們選擇最優(yōu)化的構(gòu)建方式 cargo build --release
。因為我們希望程序能運行的盡可能快!你可以從 target/release
中找到庫文件的輸出:
$ ls target/release/
build deps examples libembed.so native
這里的 libembed.so 是我們的‘共享對象’庫。我們可以像用 C 寫的對象庫一樣使用這個文件!說句題外話,根據(jù)平臺的不同,這個文件可能是 embed.dll 或是 libembed.dylib。
現(xiàn)在我們已經(jīng)構(gòu)建了我們的 Rust 庫,讓我們在 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!
$
哇,好快!在我的系統(tǒng)上,這花費了 0.086 秒的時間,而不是純 Ruby 版本花費的兩秒鐘。讓我們詳細講一下這段 Ruby 代碼:
require 'ffi
首先我們需要 ffi gem。這能讓我們像連接 C 庫一樣與 Rust 庫連接。
module Hello
extend FFI::Library
ffi_lib 'target/release/libembed.so'
ffi gem 的作者推薦使用一個模塊來圈定我們將要從共享庫中導(dǎo)入的方法的作用域。在模塊里面,我們 extend 了必要的 FFI::Library 庫模塊,然后調(diào)用 ffi_lib
來加載共享對象庫。我們只是給它傳遞了我們庫文件的存儲路徑,正如我們之前看到的,這個路徑是 target/release/libembed.so
。
attach_function :process, [], :void
attach_function
方法是由 FFI gem 提供的。它是用來連接 Rust 與 Ruby 中同名函數(shù) process() 的。因為 process() 不需要任何參數(shù),所以第二個參數(shù)是一個空數(shù)組。因為它不返回任何東西,所以我們傳遞 :void
作為最后的一個參數(shù)。
Hello.process
這才是對 Rust 的實際調(diào)用。我們的 module 和 attach_function
的調(diào)用結(jié)合才成就了它。它看起來像是一個 Ruby 函數(shù),但實際上是 Rust 的!
puts "done!"
最后,根據(jù)我們之前的項目的需求,我們打印出 done!
就是它了!正如我們所看到的,兩種語言之間的橋接是真的很容易,并且提升了很多性能。
接下來,讓我們來試試 Python 吧!
在目錄中創(chuàng)建一個 embed.py 文件,并將如下內(nèi)容輸入:
from ctypes import cdll
lib = cdll.LoadLibrary("target/release/libembed.so")
lib.process()
print("done!")
這個更簡單!我們用 ctypes 模塊中的 cdll。稍后對 LoadLibrary 的一個快速調(diào)用后,我們就可以調(diào)用 process()了。
在我的系統(tǒng)中,這花費了 0.017 秒??彀?!
Node 不是一門語言,但它目前是服務(wù)器端 JavaScript 的主要實現(xiàn)。
為了用 Node 實現(xiàn) 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(),它負責(zé)加載共享對象。我們要解釋下函數(shù)的返回類型和參數(shù)類型,返回的是‘空’,參數(shù)是一個空數(shù)組來表示的。從此之后,我們就可以調(diào)用它并打印結(jié)果。
在我的系統(tǒng)中,程序運行花費了 0.092 秒。
正如你可以看到的,這些基本操作是非常簡單的。當然,這里我們還有很多可以做的。更多的細節(jié)請查看 FFI 章節(jié)。
更多建議: