本站先前有介紹過在Rust程式語言中使用C/C++的函式庫的方式,而在這篇文章中會來談談反過來的作法,也就是在C/C++程式語言中使用Rust的函式庫。



底下先舉一個最簡單的例子,來說明如何使用C/C++程式語言呼叫Rust的函式庫。

C + Rust語言的Hello World

首先,使用以下指令建立出名為hello-world-lib的Cargo函式庫專案:

cargo new --lib hello-world-lib

src目錄下的lib.rs中,撰寫以下程式:

pub fn print_hello_world() {
    println!("Hello world!");
}

就這樣,我們的Rust函式庫就設計完成了!接下來就是決定要讓哪些函數開放出來,使外部有引用到此Rust函式庫的C/C++程式可以呼叫。當然,在這個例子中,我們只有撰寫一個print_hello_world函數,所以當然要將它公開出去。

雖然我們已經有對print_hello_world函數使用pub關鍵字,但pub關鍵字只是定義這個函數能夠被外部的Rust crate來呼叫。所以我們必須在pub關鍵字後面再加上extern關鍵字,來讓這個函數可以透過函式庫來被呼叫。

程式修改如下:

#[no_mangle]
pub extern fn print_hello_world() {
    println!("Hello world!");
}

替使用extern關鍵字來定義的函數加上#[no_mangle]屬性是為了要保留函數名稱,確保它可以被C/C++程式呼叫到。

由於Rust的函式庫預設的格式為.rlib,並非C/C++使用的.a.so.dll.dylib等。為了要編譯出C/C++語言能用的函式庫,必須要去修改Cargo的設定檔,在[lib]區塊中,加上crate-type項目,將其的值設定為["cdylib"]。如下:

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

如果不設定crate-type,Cargo會根據src目錄中有無lib.rs檔案來決定要不要加入rlib,也就是crate-type = ["rlib"]

crate-type項目加入cdylib,可以使Cargo在編譯程式專案時,將能用在C/C++程式的動態函式庫也跟著編譯出來。

將設定檔修改好後,執行cargo build --release指令,就可以在target/release目錄中找到動態函式庫檔案啦!目標平台如果是Linux的話,就會看到libhello_world_lib.so檔案。

如果想要編譯出靜態函式庫,只要在crate-type項目加入staticlib,就可以使Cargo在編譯程式專案時,將靜態函式庫也跟著編譯出來。如下:

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

目標平台如果是Linux的話,在執行cargo release指令後,在target/release目錄中就會看到libhello_world_lib.a檔案。

撰寫標頭檔

我們可以在src目錄下加上一個C/C++的hello_world_lib.h標頭檔,來對應函式庫中可被使用的函數。

寫法如下:

void print_hello_world();

撰寫C/C++程式

接著撰寫C/C++程式碼,檔名為hello_world.c,存放在Cargo程式專案的根目錄中。

#include "src/hello_world_lib.h"

int main() {
    print_hello_world();

    return 0;
}

編譯與執行C/C++程式

動態連結

在Cargo程式專案的根目錄中,執行以下指令:

gcc hello_world.c -L target/release -lhello_world_lib -o hello_world

以上指令會使用gcc來編譯hello_world.c,並且會去連結target/release中的hello_world_lib函式庫,如果動態函式庫和靜態函式庫同時存在(.a.so檔案同時存在),就會使用動態函式庫。編譯完成後,執行檔會被輸出為hello_world檔案。

在執行使用動態連結的C/C++執行檔時,動態函式庫必須要被登錄到系統環境中。在Linux環境下可以直接使用LD_LIBRARY_PATH來設定要尋找動態函式庫檔案的目錄,如下圖:

c-rust-library

而Windows環境下可以直接將.dll和執行檔放置在同一個目錄下。

靜態連結

在Cargo程式專案的根目錄中,執行以下指令:

gcc hello_world.c target/release/libhello_world_lib.a -o hello_world -ldl -pthread

以上指令會使用gcc來編譯hello_world.c,並且會去連結靜態函式庫target/release/libhello_world_lib.a。編譯完成後,執行檔會被輸出為hello_world檔案。靜態連結另外還要加上-ldl-pthread這兩個參數,因為這些是Rust應用程式基本會使用到的動態函式庫。

c-rust-library

Rust型別和C/C++型別的轉換

如果我們需要在Rust程式和C/C++程式間共享記憶體中的變數資料,就必須要透過FFI(Foreign Function Interface,外部函數介面)來完成。

稍微改寫一下剛才寫的Hello World程式來舉例吧!

use std::ffi::CStr;

#[no_mangle]
pub extern fn greet(s: *const std::os::raw::c_char, a: i32, b: i32) {
    let c_str: &CStr = unsafe { 
        CStr::from_ptr(s)
    };

    println!("Hello {}! {} + {} = {}", c_str.to_str().unwrap(), a, b, a + b);
}
#include <stdint.h>

void greet(char* s, int32_t a, int32_t b);
#include <stdio.h>

#include "src/hello_world_lib.h"

int main() {
    char* name = "Rust";
    int32_t a = 7, b = 11;

    greet(name, a, b);

    return 0;
}

重新編譯並執行C/C++程式,程式會輸出:

Hello Rust! 7 + 11 = 18