Rust 的外部函式介面(FFI)是實作與其他程式語言,特別是 C 和 C++,互操作性的關鍵機制。透過 extern 關鍵字,Rust 可以呼叫外部函式,並將 Rust 函式暴露給其他語言。理解 ABI 的重要性以及如何安全地處理指標在跨語言互動中至關重要。此外,Rust 的 FFI 也支援回呼函式,允許在 C/C++ 程式碼中呼叫 Rust 函式,實作更靈活的整合方案。在處理字串、結構體和陣列等複雜資料型別時,需要謹慎管理記憶體分配和釋放,以避免潛在的錯誤和安全風險。

第17章:外部函式介面(Foreign Function Interface)

簡介

在本章中,我們將探討Rust的外部函式介面(FFI),這是將Rust程式碼與其他程式語言整合的關鍵所在。我們將重點介紹extern關鍵字在Rust中的作用,以及它如何幫助Rust與其他語言進行互動。我們的旅程將涵蓋從C或C++呼叫Rust函式、在Rust和C++之間傳遞字串、結構和陣列、處理跨語言環境中的錯誤和異常安全性等主題。此外,我們還將展示如何為Rust函式庫建立C介面,展示Rust在不同軟體生態系統中的雙向適應性。本章最後將以從Python呼叫Rust程式碼的實際操作來作結,展現Rust的通用性和互操作性。

本章涵蓋以下主題

  • extern 關鍵字
  • 呼叫外部函式
  • 從C或C++呼叫Rust函式
  • 在Rust和C++之間傳遞字串、結構和陣列
  • 錯誤處理和異常安全性
  • 為Rust函式庫建立C介面

目標

在本章結束時,我們旨在全面瞭解Rust中的FFI,探索extern關鍵字、呼叫外部函式以及在Rust和C++之間傳遞資料等基本概念。我們將深入研究跨語言整合中的錯誤處理和異常安全性。此外,我們還將學習如何為Rust函式庫建立C介面,以促進與C和其他支援C繫結的語言之間的無縫互操作性。

extern 關鍵字

Rust中的extern關鍵字用於宣告在其他語言(如C或C++)或外部函式庫中實作的函式。對於在其他Rust crate中實作的函式,可以直接透過模組匯入呼叫,而無需使用extern。以下是使用extern宣告一個簡單函式的例子:

// 使用 'extern' 宣告外部函式
extern "C" {
    fn external_function(x: i32) -> i32;
}

fn main() {
    let result = unsafe {
        external_function(10)
    };
    println!("外部函式的結果:{}", result);
}

在此範例中,extern "C" 指定了應用二進位制介面(ABI),通知Rust使用C ABI來呼叫此函式。這對於與其他語言編寫的函式相容性至關重要。 fn external_function(x: i32) -> i32; 宣告了函式簽名但未提供其實作。實作將在外部解析,無論是在另一個Rust crate還是在外語的程式碼函式庫中。

使用extern與變數允許Rust連結到並存取在Rust外部定義的變數,例如在C程式碼中宣告的變數。下面是一個簡單的演示:

extern {
    static EXTERN_VAL: i32;
}

fn main() {
    unsafe {
        println!("外部值:{}", EXTERN_VAL);
    }
}

在上述程式碼片段中,extern用於宣告EXTERN_VAL是在外部定義的,可以從Rust程式碼中存取。 static EXTERN_VAL: i32; 宣告了EXTERN_VAL的存在而未定義其值。實際值將在與外部程式碼連結時解析。但是,請注意,使用extern會引入不安全的程式碼區塊,因為它與Rust在編譯時無法保證安全的專案互動。因此,在呼叫使用extern宣告的函式或存取變數時,需要使用不安全的區塊。

瞭解並使用extern關鍵字是使用Rust中的FFI的基礎,能夠與其他語言和外部函式庫編寫的程式碼無縫整合。

呼叫外部函式

讓我們來探索呼叫在Rust以外的語言中實作的函式的細微差別,這是與外部程式碼函式庫無縫整合的基本技能。我們的旅程將涵蓋基本函式呼叫、管理指標的複雜性,以及將回呼納入Rust生態系統。核心是extern關鍵字,它是促進Rust與外部函式之間無縫互動的樞紐。

基本函式呼叫

要從Rust呼叫外部函式,extern關鍵字開始發揮作用,允許我們宣告函式簽名並指定適當的ABI。讓我們考慮一個實際範例,從Rust呼叫一個簡單的C函式。首先,讓我們建立一個包含定義函式add_numbers的C檔案,如下所示:

// 檔案:extern_example.c
// 函式定義
int add_numbers(int a, int b) {
    return a + b;
}

將此C程式碼儲存在名為extern_example.c的檔案中。然後,將C程式碼編譯成分享函式庫。在終端機中使用以下命令:

gcc -c -o extern_example.o extern_example.c
gcc -shared -o libextern_example.so extern_example.o

這會從C程式碼建立一個名為libextern_example.so的分享函式庫。

現在,讓我們從Rust檔案呼叫C函式add_numbers,如下所示:

// 檔案:main.rs
// 宣告外部C函式
extern "C" {
    fn add_numbers(a: i32, b: i32) -> i32;
}

fn main() {
    // 呼叫外部函式
    let result = unsafe {
        add_numbers(5, 6)
    };
    // 列印結果
    println!("新增數字結果:{}", result);
}

在專案根目錄下建立一個build.rs檔案。此檔案是Rust中的特殊建置指令碼,允許您在專案建置過程中執行自訂程式碼。它通常用於連結外部函式庫(例如C/C++函式庫或系統函式庫)等任務。

// 檔案:build.rs
fn main() {
    // 取得建置指令碼的當前目錄
    let current_dir = std::env::current_dir().unwrap();
    // 告訴Cargo連結到函式庫
    println!("cargo:rustc-link-lib=dylib=extern_example");
    // 告訴Cargo在哪裡找到函式庫
    println!("cargo:rustc-link-search=native={}/src/", current_dir.display());
}

現在,使用以下命令編譯Rust程式碼,並與分享函式庫連結:

cargo run

成功編譯後,Rust程式呼叫C函式並顯示結果:

新增數字結果:11

確保C和Rust檔案位於同一目錄,或根據檔案位置調整編譯命令。

處理指標

在使用涉及指標的外部函式時,我們需要在不安全的區塊內處理它們。讓我們來檢查一個範例,示範與操縱指標的C函式之間的互動。假設有一個在C檔案中定義的名為modify_values的函式,如下所示:

// 檔案:extern_pointer_example.c
#include <stdio.h>
// 透過指標修改兩個整數值的函式定義
void modify_values(int* x, int* y) {
    *x *= 2;
    *y += 3;
}

程式碼解析:

  1. 第一段 C 程式碼:定義了一個名為 add_numbers 的簡單 C 函式,用於計算兩個整數之和。此範例展示瞭如何從 Rust 中呼叫 C 函式。

    • 作用:展示如何編寫 C 程式碼並將其編譯成分享函式庫,供 Rust 程式呼叫。
    • 邏輯:首先編寫 C 程式碼並將其編譯成分享函式庫,接著在 Rust 中宣告該 C 函式的簽名,並使用 unsafe 區塊進行呼叫。
  2. 第二段 Rust 程式碼:宣告並呼叫了 C 中的 add_numbers 函式。

    • 作用:演示如何在 Rust 中宣告並呼叫 C 函式。
    • 邏輯:透過 extern "C" 宣告 C 函式的簽名,並使用 unsafe 區塊進行呼叫,以確保相容性和安全性。
  3. build.rs 組態:組態 Cargo 編譯和連結分享函式庫。

    • 作用:指示 Cargo 如何連結到由 C 程式碼編譯而成的分享函式庫。
    • 邏輯:取得當前目錄,並指示 Cargo 連結到名為 extern_example 的動態函式庫,同時指定函式庫的位置。
  4. 第三段 C 程式碼:定義了一個名為 modify_values 的 C 函式,用於修改兩個整數指標的值。

    • 作用:展示如何在 C 中操作指標,並準備從 Rust 中呼叫。
    • 邏輯:該 C 函式接受兩個整數指標作為引數,並對其所指向的值進行修改。

此範例全面展示了 Rust 如何透過 FFI 與 C 程式碼進行互動,包括基本的函式呼叫和涉及指標的操作,同時保證了操作的正確性和安全性。

圖表翻譯:

此圖示描述了 Rust 與 C 語言之間的互動過程,包括編譯、連結及執行階段的操作流程。

  1. 編寫 C 程式碼並編譯成分享函式庫。
  2. 在 Rust 中宣告並呼叫 C 函式。
  3. 使用 build.rs 組態 Cargo 以正確連結分享函式庫。
  4. 編譯並執行 Rust 程式,完成與 C 程式碼的互動。

圖表翻譯:

此圖示展示了 Rust 與 C 語言之間的互動過程。主要步驟包括:

  • 編寫並編譯 C 程式碼為分享函式庫。
  • 在 Rust 中宣告 C 函式的簽名並進行呼叫。
  • 使用 build.rs 組態連結過程。
  • 編譯並執行 Rust 程式,完成跨語言互動。

Rust 與 C/C++ 的跨語言互動實務

Rust 的 Foreign Function Interface (FFI) 提供了一種機制,能夠與其他程式語言(如 C 和 C++)進行互動。本章將探討 Rust 如何與 C/C++ 程式碼互操作,包括呼叫 C 函式、處理回呼函式、以及在 Rust 和 C/C++ 之間傳遞字串、結構體和陣列等資料型別。

從 Rust 呼叫 C 函式

Rust 可以呼叫 C 函式,但需要使用 extern "C" 關鍵字宣告 C 函式的介面。以下是一個範例,展示如何從 Rust 呼叫一個 C 函式來修改整數的值:

// C code: modify_values.c
void modify_values(int* x, int* y) {
    *x = 10;
    *y = 20;
}
// Rust code: main.rs
extern "C" {
    fn modify_values(x: *mut i32, y: *mut i32);
}

fn main() {
    let mut a = 5;
    let mut b = 7;
    println!("Before modification: a = {}, b = {}", a, b);
    unsafe {
        modify_values(&mut a, &mut b);
    }
    println!("After modification: a = {}, b = {}", a, b);
}

內容解密:

  1. 在 Rust 中使用 extern "C" 宣告 C 函式 modify_values
  2. modify_values 函式接受兩個 *mut i32 指標,並修改它們所指向的值。
  3. 在 Rust 的 main 函式中,宣告兩個可變整數 ab,並列印它們的初始值。
  4. 使用 unsafe 區塊呼叫 modify_values,並傳遞 ab 的可變參考。
  5. 列印修改後的值。

處理回呼函式

Rust 的 FFI 也支援處理回呼函式,能夠將 Rust 的閉包或函式指標傳遞給 C 函式。以下是一個範例:

// C code: extern_callback_example.c
void perform_operation(int a, int b, void (*callback)(int)) {
    int result = a + b;
    callback(result);
}
// Rust code: main.rs
extern "C" {
    fn perform_operation(a: i32, b: i32, callback: extern "C" fn(i32));
}

fn main() {
    extern "C" fn callback_function(result: i32) {
        println!("Callback result: {}", result);
    }
    unsafe {
        perform_operation(10, 5, callback_function);
    }
}

內容解密:

  1. 在 Rust 中宣告 C 函式 perform_operation,它接受兩個整數和一個回呼函式指標。
  2. 定義一個 Rust 函式 callback_function,並使用 extern "C" 使其與 C 相容。
  3. main 函式中,使用 unsafe 區塊呼叫 perform_operation,並傳遞 callback_function
  4. perform_operation 計算結果後呼叫回呼函式,將結果傳遞給 callback_function

從 C 呼叫 Rust 函式

要從 C 呼叫 Rust 函式,需要建立一個與 C 相容的介面。以下是一個範例:

// File: lib.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}
// File: main.c
#include <stdio.h>

extern int add(int a, int b);

int main() {
    int result = add(5, 6);
    printf("Result of adding numbers: %d\n", result);
    return 0;
}

內容解密:

  1. 在 Rust 中定義一個函式 add,並使用 #[no_mangle]extern "C" 使其與 C 相容。
  2. 在 C 程式碼中宣告 add 函式,並呼叫它。
  3. 編譯 Rust 程式碼為動態連結函式庫,並將其連結到 C 程式碼。

在 Rust 和 C++ 之間傳遞字串

在 Rust 和 C++ 之間傳遞字串需要小心處理記憶體管理。以下是一個範例:

// File: lib.rs
use std::ffi::CString;
use std::os::raw::c_char;

#[no_mangle]
pub extern "C" fn process_string(input: *const c_char) -> *mut c_char {
    let input_str = unsafe { CString::from_raw(input as *mut c_char) };
    let original_string = input_str.to_str().unwrap();
    let processed_string = original_string.chars().rev().collect::<String>();
    CString::new(processed_string).unwrap().into_raw()
}

內容解密:

  1. 使用 CString 將輸入的 C 風格字串轉換為 Rust 字串。
  2. 處理字串(在此範例中,將字串反轉)。
  3. 將處理後的字串轉換為 CString,並傳回其原始指標。