iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 21
2
Software Development

30 天深入淺出 Rust系列 第 21

用 Rust 呼叫 C 的程式

若是其它的函式庫等等的, crates.io 上可能已經有人提供與那個函式庫的綁定了,可以直接抓來用, Rust 的 crate 的命名慣例中有個若一個 crate 是以 -sys 結尾,代表它提供的是最基礎的綁定,通常它的 API 都是 unsafe 的,若要使用還要花點工夫,也可以找找有沒有高階一點的綁定的函式庫。

綁定 C 函式庫

此篇教學的程式碼會在 https://github.com/DanSnow/rust-intro/tree/master/c-binding

假設我們用 C 寫了個函式庫:

#include <stdio.h>

void say_hello(const char *message) {
  printf("%s from C", message);
}

這個檔案我們放在 lib/hello.c

然後我們要在 Rust 的程式中呼叫:

use std::{ffi::CString, os::raw::c_char};

extern "C" {
  // 宣告一個外部的函式,傳入一個 C 的 char 指標
  fn say_hello(message: *const c_char);
}

fn main() {
  // 建立一個 C 的字串,也就是由 \0 結尾的字串
  let msg = CString::new("Hello").unwrap();
  // 呼叫,並把字串轉換成指標傳入
  unsafe { say_hello(msg.as_ptr()) };
}

字串的部份應該是連結 C 的函式庫時最麻煩的一塊了,建議可以看一下文件的 std::ffi::CString 與跟它搭配的 CStr ,它們就是 Rust 中的 Stringstr 的 C 的版本,通常就是用來傳給 C 的函式庫用的。

這邊我們先嘗試自己手動建置 C 的函式庫,與連結吧,為了簡化這個過程我把建置的過程全部寫成了一個 Makefile 不過這並不是本教學的重點,可以直接去看程式碼的 Makefile 怎麼寫的,也可以上網找找要怎麼寫。

假設我們已經把 C 的部份建置好了,並放在 lib/libhello.a ,接著我們要讓 cargo 知道我們的程式還要去連結這個函式庫才能編譯,我們需要在根目錄下建一個 build.rs ,然後在裡面寫進這樣的程式碼:

fn main() {
  // 告訴 cargo 我們要連結一個靜態的叫 libhello.a 的函式庫
  println!("cargo:rustc-link-lib=static=hello");
  // 告訴 cargo 要加上這個搜尋函式庫的位置
  println!(
    "cargo:rustc-link-search={}",
    // 這邊為了簡化直接使用 concat! 這個 macro 來做字串的連結
    // 正常應該是要用 std::path::PathBuf 之類的來處理不同系統下的不同分隔符號
    // 因為在 Linux 下的分隔符號是 / 但在 Windows 下是 \
    concat!(
      // 這個會是專案的目錄
      env!("CARGO_MANIFEST_DIR"),
      // 我們的函式庫所在的位置
      "/lib"
    )
  );
}

cargo 在建置時會執行 build.rs 並依照裡面的特殊的輸出來處理,詳細可以去看看 cargo 的文件 ,裡面有詳細寫出這些特殊的輸出的用法,此外 build.rs 也可以提供一些不同的功能,比如使用 vergen ,這個 crate 可以幫助我們把目前的版本號存到環境變數,然後我們就可以在編譯時從環境變數讀進目前的版本號一起編譯進程式裡。

不過這樣我們就要靠自己寫 Makefile 來先建置 C 的函式庫才有辦法編譯,有點麻煩,當然 Rust 的社群也想到了這個問題,於是就做了個叫 cc 的 crate ,它能讓我們在 build.rs 中指定要編譯與連結的 C 函式庫,首先我們要先把它加進我們的的 Cargo.toml ,我們可以用這個指令:

$ cargo add cc --build

這個指令要安裝 cargo-edit 才有喔,沒裝的話請去看本系列第三篇

然後把我們的 build.rs 改成:

extern crate cc;

fn main() {
  cc::Build::new().file("lib/hello.c").compile("hello");
}

就這樣,超簡單的, Makefile 也不需要了, cargo 在建置的過程就會去呼叫 gcc 來幫忙編譯 C 的函式庫,並做連結了。

如果要連結系統中的函式庫可以看看 pkg-config 這個 crate ,這應該是 Linux 系統下大部份都支援的一個方式,或是直接使用上面所提到 build.rs 的使用方式來連結系統的函式庫。

bindgen

https://github.com/rust-lang-nursery/rust-bindgen

bindgen 是個由 Rust 寫成的工具,用途是從 C 或 C++ 的標頭檔自動產生 Rust 的綁定,原本這個工具是 Mozilla 所開發的,不過目前已經轉移給 Rust 的社群了,它有指令介面與用在 build.rs 兩種用法,先介紹指令介面,首先先來安裝:

$ cargo install bindgen

接著我們幫剛剛的 hello.c 定義個標頭檔:

#ifndef HELLO_H_INCLUDE
#define HELLO_H_INCLUDE

void say_hello(const char *message);

#endif

使用指令:

$ bindgen hello.h

應該會看到這樣的輸出:

/* automatically generated by rust-bindgen */

extern "C" {
  pub fn say_hello(message: *const ::std::os::raw::c_char);
}

其實這跟我們自己寫的沒什麼兩樣,不過因為是自動產生的,若要幫一些比較大的第三方函式庫做綁定時能自動產生就很方便,於是上面的程式碼就可以直接使用,然後像上面介紹的方法一樣去連結與建置了。

接下來我們用 build.rs 來產生綁定:

extern crate bindgen;
extern crate cc;

use std::{env, path::PathBuf};

fn main() {
  // 剛剛的程式碼
  cc::Build::new().file("lib/hello.c").compile("hello");

  let bindings = bindgen::Builder::default()
    .header("lib/hello.h")
    .generate()
    .expect("Unable to generate bindings");

  // 這會寫到 Rust 所用的暫存資料夾
  let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
  bindings
    .write_to_file(out_dir.join("bindings.rs"))
    .expect("Couldn't write bindings!");
}

然後我們讓原本的程式碼改成引入自動產生的綁定:

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

再建置一次,應該會成功,不過時間會花的比較久,因為 bindgen 有點大。

下一篇再來介紹如何從 C 呼叫 Rust 的程式碼。


上一篇
Crates 與工具
下一篇
從 C 呼叫 Rust
系列文
30 天深入淺出 Rust33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言