Go 和 Rust 我都要!

張晉濤發表於2021-11-29

大家好,我是張晉濤。

近期 Rust 社群/團隊有些變動,所以再一次將 Rust 拉到大多數人眼前。

我最近看到很多小夥伴說的話:

Rust 還值得學嗎?社群是不是不穩定呀

Rust 和 Go 哪個好?

Rust 還值得學嗎?

這些問題如果有人來問我,那我的回答是:

小孩子才做選擇,我都要!

當然,關於 Rust 和 Go 的問題也不算新,比如之前的一條推文:

我在本篇中就來介紹下如何用 Go 呼叫 Rust。

當然,這篇中我基本上不會去比較 Go 和 Rust 的功能,或者這種方式的效能之類的,Just for Fun

FFI 和 Binding

FFI (Foreign Function Interface) 翻譯過來叫做外部函式介面(為了比較簡單,下文中都將使用 FFI 指代)。最早來自於 Common Lisp 的規範,這是在 wiki 上寫的,我並沒有去考證。
不過我所使用過的絕大多數語言中都有 FFI 的概念/術語存在,比如:Python、Ruby, Haskell、Go、Rust、LuaJIT 等。

FFI 的作用簡單來說就是允許一種語言去呼叫另一種語言,有時候我們也會用 Binding 來表示類似的能力。

在不同的語言中會有不同的實現,比如在 Go 中的 cgo , Python 中的 ctypes , Haskell 中的 CAPI (之前還有一個 ccall)等。
我個人感覺 Haskell 中用 FFI 相比其他語言要更簡單&方便的多,不過這不是本篇的重點就不展開了。

在本文中,對於 Go 和 Rust 而言,它們的 FFI 需要與 C 語言物件進行通訊,而這部分其實是由作業系統根據 API 中的呼叫約定來完成的。

我們來進入正題。

準備 Rust 示例程式

Rust 的安裝和 Cargo 工具的基本使用,這裡就不介紹了。大家可以去 Rust 的官網進行了解。

用 Cargo 建立專案

我們先準備一個目錄用來放本次示例的程式碼。(我建立的目錄叫做 go-rust

然後使用 Rust 的 Cargo 工具建立一個名叫 rustdemo 的專案,這裡由於我增加了 --lib 的選項,使用其內建的 library 模板。

➜  go-rust git:(master) ✗ mkdir lib && cd lib
➜  go-rust git:(master) ✗ cargo new --lib rustdemo
     Created library `rustdemo` package
➜  go-rust git:(master) ✗ tree rustdemo 
rustdemo
├── Cargo.toml
└── src
    └── lib.rs

1 directory, 2 files

準備 Rust 程式碼

extern crate libc;
use std::ffi::{CStr, CString};

#[no_mangle] 
pub extern "C" fn rustdemo(name: *const libc::c_char) -> *const libc::c_char {
    let cstr_name = unsafe { CStr::from_ptr(name) };
    let mut str_name = cstr_name.to_str().unwrap().to_string();
    println!("Rust get Input:  \"{}\"", str_name);
    let r_string: &str = " Rust say: Hello Go ";
    str_name.push_str(r_string);
    CString::new(str_name).unwrap().into_raw()
}

程式碼比較簡單,Rust 暴露出來的函式名叫做 rustdemo ,接收一個外部的引數,並將其列印出來。之後從 Rust 這邊再設定一個字串。

CString::new(str_name).unwrap().into_raw() 被轉換為原始指標,以便之後由 C 語言處理。

編譯 Rust 程式碼

我們需要修改下 Cargo.toml 檔案以便進行編譯。注意,這裡我們增加了 crate-type = ["cdylib"]libc

[package]
name = "rustdemo"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
libc = "0.2"

然後進行編譯

➜  rustdemo git:(master) ✗ cargo build --release
   Compiling rustdemo v0.1.0 (/home/tao/go/src/github.com/tao12345666333/go-rust/lib/rustdemo)
    Finished release [optimized] target(s) in 0.22s

檢視生成的檔案,這是一個 .so 檔案(這是因為我在 Linux 環境下,你如果在其他系統環境下會不同)

➜  rustdemo git:(master) ✗ ls target/release/librustdemo.so 
target/release/librustdemo.so

準備 Go 程式碼

Go 環境的安裝之類的這裡也不再贅述了,繼續在我們的 go-rust 目錄操作即可。

編寫 main.go

package main

/*
#cgo LDFLAGS: -L./lib -lrustdemo
#include <stdlib.h>
#include "./lib/rustdemo.h"
*/
import "C"

import (
    "fmt"
    "unsafe"
)

func main() {
    s := "Go say: Hello Rust"

    input := C.CString(s)
    defer C.free(unsafe.Pointer(input))
    o := C.rustdemo(input)
    output := C.GoString(o)
    fmt.Printf("%s\n", output)
}

在這裡我們使用了 cgo ,在 import "C" 之前的註釋內容是一種特殊的語法,這裡是正常的 C 程式碼,其中需要宣告使用到的標頭檔案之類的。

下面的程式碼很簡單,定義了一個字串,傳遞給 rustdemo 函式,然後列印 C 處理後的字串。

同時,為了能夠讓 Go 程式能正常呼叫 Rust 函式,這裡我們還需要宣告其標頭檔案,在 lib/rustdemo.h 中寫入如下內容:

char* rustdemo(char *name);

編譯程式碼

在 Go 編譯的時候,我們需要開啟 CGO (預設都是開啟的),同時需要連結到 Rust 構建出來的 rustdemo.so 檔案,所以我們將該檔案和它的標頭檔案放到 lib 目錄下。

➜  go-rust git:(master) ✗ cp lib/rustdemo/target/release/librustdemo.so lib

所以完整的目錄結構就是:

➜  go-rust git:(master) ✗ tree -L 2 .
.
├── go.mod
├── lib
│   ├── librustdemo.so
│   ├── rustdemo
│   └── rustdemo.h
└── main.go

2 directories, 5 files

編譯:

➜  go-rust git:(master) ✗ go build -o go-rust  -ldflags="-r ./lib" main.go
➜  go-rust git:(master) ✗ ./go-rust 
Rust get Input:  "Go say: Hello Rust"
Go say: Hello Rust Rust say: Hello Go

可以看到,第一行的輸出是由 Go 傳入了 Rust , 第二行中則是從 Rust 再傳回 Go 的了。符合我們的預期。

總結

本篇介紹瞭如何使用 Go 與 Rust 進行結合,介紹了其前置關於 FFI 相關的知識,後續通過一個小的實踐演示了其完整過程。
感興趣的小夥伴可以自行實踐下。


歡迎訂閱我的文章公眾號【MoeLove】

相關文章