在系統開發中,結合 Rust 的安全性和 C 的效能常常是必要的。本文將介紹如何使用 bindgen 自動生成 C 函式繫結,讓 Rust 程式碼可以直接呼叫 C 函式,以及如何使用 cc crate 在 Rust 專案中編譯 C 程式碼。此外,我們將以簡化的原子參照計數器 (Arc) 實作為例,示範如何在 Rust 中安全地管理分享資料,並探討 Makefile 的編寫與跨語言專案的建置流程。透過這些技巧,開發者可以更有效地整合 Rust 和 C,發揮兩者的優勢。

使用 bindgen 生成 C 函式繫結

要在 Rust 中使用 C 函式,需要使用 bindgen 來生成 C 函式的繫結。bindgen 是一種工具,能夠自動生成 C 函式的繫結,讓 Rust 可以使用 C 函式。以下是使用 bindgen 的步驟:

  1. 安裝 bindgencc
$ sudo apt-get update && sudo apt-get upgrade
$ sudo apt-get install clang-14 --install-suggests
$ sudo apt-get install make
  1. 建立一個新專案,例如 add_ffi
$ cargo new add_ffi
  1. Cargo.toml 中新增 bindgencc 的依賴:
[dependencies]
cc = "1.0.73"
bindgen = "0.59.2"
  1. 建立一個 build.rs 檔案,內容如下:
use std::env;
use std::path::PathBuf;

fn main() {
    let out = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindgen::Builder::default()
        .header("add.h")
        .generate()
        .expect("Unable to generate bindings")
        .write_to_file(out.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}
  1. 建立一個 add.h 檔案,內容如下:
int add(int a, int b);
  1. 建立一個 add.c 檔案,內容如下:
int add(int a, int b) {
    return a + b;
}
  1. 執行 cargo build 來生成繫結和編譯 C 程式碼。

使用 C 函式

現在,可以在 Rust 中使用 C 函式了。以下是範例程式碼:

extern crate add_ffi;

use add_ffi::bindings::*;

fn main() {
    let a = 2;
    let b = 3;
    let result = add(a, b);
    println!("{} + {} = {}", a, b, result);
}

這個範例程式碼使用 add 函式來計算 ab 的總和,並將結果印出來。

使用 Rust 和 C 共同開發:一個簡單的範例

在這個範例中,我們將使用 Rust 和 C 共同開發一個簡單的加法函式。首先,我們需要建立一個新的 Rust 專案,並新增必要的依賴項。

新增依賴項

Cargo.toml 檔案中,新增以下依賴項:

[build-dependencies]
bindgen = "0.60.1"
cc = "1.0.73"

建立 C 程式

首先,建立一個新的目錄 includes,並在其中建立一個新的檔案 add.h。在 add.h 檔案中,新增以下程式碼:

#ifndef ADD_H
#define ADD_H

#include <stdint.h>
uint32_t add(uint32_t a, uint32_t b);

#endif

接下來,建立一個新的檔案 add.c,並在其中新增以下程式碼:

#include "includes/add.h"

uint32_t add(uint32_t a, uint32_t b){
    return a + b;
}

建立 Rust Build Script

在專案根目錄中,建立一個新的檔案 build.rs,並在其中新增以下程式碼:

use bindgen::Builder;
use cc::Build;

use std::path::PathBuf;

fn main() {
    // 編譯 C 程式
    Build::new()
        .file("add.c")
        .include("includes")
        .compile("add.so");

    // 生成 Rust Bindings
    let bindings = Builder::default()
        .header("includes/add.h")
        .generate()
        .unwrap();
}

使用 Rust Bindings

現在,我們可以使用 Rust Bindings 來呼叫 C 函式。建立一個新的檔案 main.rs,並在其中新增以下程式碼:

extern crate add;

fn main() {
    let result = unsafe { add::add(2, 3) };
    println!("Result: {}", result);
}

執行程式

執行以下命令來編譯和執行程式:

cargo build
cargo run

這將輸出 Result: 5

使用Rust與C進行混合開發

在進行混合開發時,需要考慮到Rust和C的語言差異以及如何進行溝通。以下是使用Rust與C進行混合開發的步驟:

使用bindgen生成Rust繫結

首先,需要使用bindgen生成Rust繫結。bindgen是一個工具,可以自動生成Rust繫結,讓Rust可以呼叫C程式碼。以下是使用bindgen的步驟:

  1. 安裝bindgen:cargo install bindgen
  2. 建立一個新的Rust專案:cargo new myproject
  3. 在專案中新增C程式碼:touch src/main.c
  4. 使用bindgen生成Rust繫結:bindgen --output src/bindings.rs src/main.c

使用cc生成C繫結

如果需要在C中呼叫Rust程式碼,需要使用cc生成C繫結。cc是一個工具,可以自動生成C繫結,讓C可以呼叫Rust程式碼。以下是使用cc的步驟:

  1. 安裝cc:cargo install cc
  2. 建立一個新的Rust專案:cargo new myproject
  3. 在專案中新增Rust程式碼:touch src/main.rs
  4. 使用cc生成C繫結:cc --output src/main.c src/main.rs

使用extern進行溝通

在Rust和C之間進行溝通時,需要使用extern關鍵字。extern關鍵字可以讓Rust呼叫C程式碼,也可以讓C呼叫Rust程式碼。以下是使用extern的步驟:

  1. 在Rust中使用extern關鍵字呼叫C程式碼:extern "C" { fn my_c_function(); }
  2. 在C中使用extern關鍵字呼叫Rust程式碼:extern void my_rust_function();

範例

以下是使用Rust與C進行混合開發的範例:

// src/main.rs
extern "C" {
    fn add(a: i32, b: i32) -> i32;
}

fn main() {
    let result = unsafe { add(2, 3) };
    println!("The result is: {}", result);
}
// src/main.c
int add(int a, int b) {
    return a + b;
}

在這個範例中,Rust程式碼呼叫C程式碼的add函式,然後列印預出結果。

Rust 和 C 的混合開發

建立 Makefile

首先,建立一個 Makefile 來管理專案的編譯和連結過程。Makefile 的內容如下:

build:
    cargo build
    clang main.c -L ./target/debug -lrust_in_c -o main

這個 Makefile 定義了一個 build 指令,該指令先使用 cargo build 編譯 Rust 程式碼,然後使用 clang 編譯 C 程式碼,並連結 Rust 程式碼生成的動態函式庫。

C 程式碼

接下來,編寫 C 程式碼 main.c。這個程式碼將使用 Rust 程式碼生成的動態函式庫來計算兩個檔案之間的字元差異。

#include <stdlib.h>
#include <stdio.h>

int diff_strings(char* s1, char* s2);
char* read_file(char* path);

int main() {
    char* cargoToml = read_file("Cargo.toml");
    char* libRs = read_file("src/lib.rs");
    printf("Result = %d characters", diff_strings(cargoToml, libRs));
    free((void*)cargoToml);
    free((void*)libRs);
    return 0;
}

這個 C 程式碼定義了兩個函式 diff_stringsread_file,並使用這些函式來計算兩個檔案之間的字元差異。

Rust 程式碼

接下來,編寫 Rust 程式碼 src/lib.rs。這個程式碼將提供兩個函式 diff_stringsread_file,這些函式將被 C 程式碼呼叫。

use std::ffi::{c_char, c_int, CStr, CString};
use std::fs::read_to_string;

#[no_mangle]
pub extern "C" fn diff_strings(s1: *mut c_char, s2: *mut c_char) -> c_int {
    unsafe {
        let s1_cstr = CStr::from_ptr(s1);
        let s1_str = s1_cstr.to_str().unwrap();
        let s1_len = s1_str.len();
        let s2_cstr = CStr::from_ptr(s2);
        let s2_str = s2_cstr.to_str().unwrap();
        let s2_len = s2_str.len();
        s2_len.abs_diff(s1_len) as c_int
    }
}

#[no_mangle]
pub extern "C" fn read_file(path: *mut c_char) -> *mut c_char {
    // ...
}

這個 Rust 程式碼定義了兩個函式 diff_stringsread_file,這些函式將被 C 程式碼呼叫。函式 diff_strings 計算兩個字串之間的字元差異,函式 read_file 讀取一個檔案的內容並傳回一個字串。

Cargo.toml

最後,編寫 Cargo.toml 檔案來組態 Rust 專案。

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

這個 Cargo.toml 檔案組態了 Rust 專案為動態函式庫,並命名為 rust_in_c

執行

使用 make build 指令編譯和連結專案,然後執行生成的 main 可執行檔案。

$ make build
$ ./main

這將輸出兩個檔案之間的字元差異。

混合語言開發:Rust 與 C 的整合

在本章中,我們將探討如何使用 Rust 進行混合語言開發,特別是與 C 的整合。Rust 提供了一種安全且高效的方式來進行系統程式設計,而 C 則是一種成熟且廣泛使用的語言。透過整合這兩種語言,我們可以發揮各自的優點,創造出更強大和更高效的應用程式。

使用 extern "C" 來定義 C 函式

要在 Rust 中使用 C 函式,我們需要使用 extern "C" 來定義它們。這個關鍵字告訴 Rust 編譯器,這個函式是使用 C 的呼叫約定來定義的。以下是一個例子:

#[no_mangle]
pub extern "C" fn read_file(path: *mut c_char) -> *mut c_char {
    // ...
}

在這個例子中,我們定義了一個名為 read_file 的 C 函式,它接受一個 *mut c_char 型別的引數 path,並傳回一個 *mut c_char 型別的值。

使用 CStr 來轉換 C 字串

要在 Rust 中使用 C 字串,我們需要使用 CStr 來轉換它們。以下是一個例子:

use std::ffi::CStr;

let path_cstr = CStr::from_ptr(path);
let path_str = path_cstr.to_str().unwrap();

在這個例子中,我們使用 CStr 來轉換一個 C 字串 path 成為一個 Rust 字串 path_str

使用 CString 來建立 C 字串

要在 Rust 中建立一個 C 字串,我們需要使用 CString。以下是一個例子:

use std::ffi::CString;

let content_cstr = CString::new(content).unwrap();

在這個例子中,我們使用 CString 來建立一個 C 字串 content_cstr

使用 into_raw 來傳回 C 字串

要傳回一個 C 字串,我們需要使用 into_raw 來轉換它成一個 *mut c_char 型別的指標。以下是一個例子:

content_cstr.into_raw()

在這個例子中,我們使用 into_raw 來轉換一個 CString 成為一個 *mut c_char 型別的指標。

執行程式

要執行程式,我們需要使用 cargo build 來編譯 Rust 程式碼,然後使用 clang 來編譯 C 程式碼。以下是一個例子:

$ make build
cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
clang main.c -L ./target/debug -lrust_in_c -o main

在這個例子中,我們使用 cargo build 來編譯 Rust 程式碼,然後使用 clang 來編譯 C 程式碼。

重點

  • 使用 extern "C" 來定義 C 函式
  • 使用 CStr 來轉換 C 字串
  • 使用 CString 來建立 C 字串
  • 使用 into_raw 來傳回 C 字串
  • 使用 cargo build 來編譯 Rust 程式碼
  • 使用 clang 來編譯 C 程式碼

練習

  • 實作一個使用 extern "C" 來定義 C 函式的 Rust 程式
  • 實作一個使用 CStr 來轉換 C 字串的 Rust 程式
  • 實作一個使用 CString 來建立 C 字串的 Rust 程式
  • 實作一個使用 into_raw 來傳回 C 字串的 Rust 程式

圖表翻譯:

  graph LR
    A[Rust] -->|extern "C"|> B[C 函式]
    B -->|CStr|> C[Rust 字串]
    C -->|CString|> D[C 字串]
    D -->|into_raw|> E[*mut c_char]
    E -->|傳回|> F[C 程式]

在這個圖表中,我們展示瞭如何使用 extern "C" 來定義 C 函式,使用 CStr 來轉換 C 字串,使用 CString 來建立 C 字串,使用 into_raw 來傳回 C 字串。

實作簡化版的 Atomic Reference Counter (Arc)

1. 建立 Arc 結構體

首先,我們需要建立一個名為 Arc 的結構體,它將包含指向 ArcInner 的指標和一個標記,表明我們對內部值具有所有權。ArcInner 結構體將包含參照計數和內部資料。

use std::marker::PhantomData;
use std::ptr::NonNull;
use std::sync::atomic::{self, AtomicUsize, Ordering};
use std::ops::Deref;

// 原子參照計數器
pub struct Arc<T> {
    // 指向 ArcInner 的指標
    ptr: NonNull<ArcInner<T>>,
    // 標記,表明我們對 ArcInner 具有所有權
    phantom: PhantomData<ArcInner<T>>,
}

// ArcInner 結構體,包含參照計數和內部資料
pub struct ArcInner<T> {
    // 參照計數
    rc: AtomicUsize,
    // 內部資料
    data: T,
}

2. 實作 new 方法

接下來,我們需要實作 new 方法,該方法建立一個新的 Arc 例項。

impl<T> Arc<T> {
    // 建立一個新的 Arc 例項
    pub fn new(data: T) -> Arc<T> {
        // 建立一個新的 ArcInner 例項,參照計數為 1
        let boxed = Box::new(ArcInner {
            rc: AtomicUsize::new(1),
            data,
        });
        
        // 建立一個新的 NonNull 指標,指向 ArcInner
        let ptr = NonNull::new(boxed.as_ref()).unwrap();
        
        // 將 ArcInner 轉換為 raw 指標,並建立 Arc 例項
        let arc = Arc {
            ptr,
            phantom: PhantomData,
        };
        
        // 傳回 Arc 例項
        arc
    }
}

3. 實作 clone 方法

現在,我們需要實作 clone 方法,該方法建立一個新的 Arc 例項,並增加參照計數。

impl<T> Clone for Arc<T> {
    fn clone(&self) -> Self {
        // 增加參照計數
        self.ptr.as_ref().rc.fetch_add(1, Ordering::SeqCst);
        
        // 建立一個新的 Arc 例項,指向相同的 ArcInner
        Arc {
            ptr: self.ptr,
            phantom: PhantomData,
        }
    }
}

4. 實作 drop 方法

最後,我們需要實作 drop 方法,該方法減少參照計數,並在參照計數為 0 時釋放內部資料。

impl<T> Drop for Arc<T> {
    fn drop(&mut self) {
        // 減少參照計數
        let count = self.ptr.as_ref().rc.fetch_sub(1, Ordering::SeqCst);
        
        // 如果參照計數為 0,釋放內部資料
        if count == 1 {
            let _ = unsafe { Box::from_raw(self.ptr.as_ptr()) };
        }
    }
}

5. 實作 Deref 特徵

為了能夠使用 * 來存取內部資料,我們需要實作 Deref 特徵。

impl<T> Deref for Arc<T> {
    type Target = T;
    
    fn deref(&self) -> &Self::Target {
        &self.ptr.as_ref().data
    }
}

現在,你可以使用簡化版的 Arc 來管理分享資料了。這個實作提供了基本的參照計數和內部資料存取功能,但它並不如標準函式庫中的 Arc 那樣強大和安全。

實作原子參考計數器

為了實作一個原子參考計數器(Atomic Reference Counter, Arc),我們需要定義一個結構體來儲存指向內部資料的指標和參考計數。以下是 Arc 的基本實作:

use std::marker::PhantomData;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc as StdArc;

// 定義 ArcInner 結構體
struct ArcInner<T> {
    data: T,
    rc: AtomicUsize,
}

// 定義 Arc 結構體
pub struct Arc<T> {
    ptr: std::ptr::NonNull<ArcInner<T>>,
    phantom: PhantomData<T>,
}

impl<T> Arc<T> {
    // 建立一個新的 Arc 例項
    pub fn new(data: T) -> Self {
        let inner = Box::new(ArcInner {
            data,
            rc: AtomicUsize::new(1),
        });
        let ptr = std::ptr::NonNull::new(Box::into_raw(inner)).unwrap();
        Arc {
            ptr,
            phantom: PhantomData,
        }
    }

    // 取得 Arc 的參考計數
    pub fn count(&self) -> usize {
        let inner = unsafe { self.ptr.as_ref() };
        inner.rc.load(Ordering::Acquire)
    }

    // 克隆 Arc
    pub fn clone(&self) -> Self {
        let inner = unsafe { self.ptr.as_ref() };
        let rc = inner.rc.fetch_add(1, Ordering::Relaxed);
        if rc == usize::MAX {
            panic!("Reference count overflow");
        }
        Arc {
            ptr: self.ptr,
            phantom: PhantomData,
        }
    }
}

// 實作 Deref 特徵
impl<T> std::ops::Deref for Arc<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        let inner = unsafe { self.ptr.as_ref() };
        &inner.data
    }
}

// 實作 Send 和 Sync 特徵
unsafe impl<T: Sync + Send> Send for Arc<T> {}
unsafe impl<T: Sync + Send> Sync for Arc<T> {}

這個實作提供了基本的 Arc 功能,包括建立、克隆和取得參考計數。它使用 std::sync::atomic 來實作原子操作,確保參考計數的更新是執行緒安全的。同時,它也實作了 Deref 特徵,允許使用 * 運算子來取得 Arc 內的資料。最後,它實作了 SendSync 特徵,允許 Arc 在多執行緒環境中安全地分享和傳遞。

圖表翻譯:

  classDiagram
    class Arc~T~ {
        - ptr: std::ptr::NonNull~ArcInner~T~~
        - phantom: PhantomData~T~
        + new(data: T) Arc~T~
        + count() usize
        + clone() Arc~T~
    }
    class ArcInner~T~ {
        - data: T
        - rc: AtomicUsize
    }
    Arc~T~ --* ArcInner~T~

這個圖表展示了 Arc 和 ArcInner 的關係,Arc 內部包含一個指向 ArcInner 的指標和一個 PhantomData 例項。ArcInner 則包含了實際的資料和參考計數。這個圖表有助於理解 Arc 的內部結構和其與 ArcInner 的關係。

從底層實作到高階應用的全面檢視顯示,Rust 與 C 的混合開發,本質上是透過外部函式介面 (FFI) 實作兩種語言間的溝通橋樑。bindgen 工具的運用,自動化產生 Rust 繫結,大幅簡化了整合 C 程式碼的複雜度,讓開發者得以更專注於業務邏輯的開發。分析 C 程式碼與 Rust 程式碼的互動模式,可以發現,透過 extern "C" 宣告以及 CStrCString 等工具的輔助,Rust 能夠有效地操作 C 的資料結構,實作底層效能與高階安全性的巧妙平衡。然而,FFI 的使用也存在潛在風險,例如記憶體管理的責任轉移,需要開發者更謹慎地處理指標和記憶體釋放,避免出現記憶體洩漏或 dangling pointer 等問題。展望未來,隨著 Rust 語言的日漸成熟以及社群的蓬勃發展,預期將會有更多工具和最佳實務出現,進一步降低混合語言開發的門檻。對於追求效能與安全的系統級開發,Rust 與 C 的整合將成為主流趨勢,值得技術團隊深入研究並積極應用於實務專案中。玄貓認為,掌握 Rust 與 C 混合開發的技巧,將賦予開發者更大的彈性,在效能、安全性和開發效率之間取得最佳平衡。