はじめに
Rustは安全な言語。でも、既存のCライブラリを使いたいときがある。
- システムコール
- 高速な数値計算ライブラリ
- レガシーコード
そんなとき使うのが FFI (Foreign Function Interface)。
この記事では、CライブラリをRustから安全に呼び出す方法を解説します。
目次
FFI の基礎
FFI とは
Foreign Function Interface - 他の言語で書かれた関数を呼び出す仕組み。
Rust では主に C 言語との相互運用を指します。
// C の関数を Rust から呼ぶ
extern "C" {
fn abs(x: i32) -> i32;
}
fn main() {
unsafe {
println!("{}", abs(-5)); // 5
}
}
なぜ unsafe?
C言語の世界には Rust の安全性保証がない:
- null ポインタの可能性
- メモリ安全性の保証なし
- ダングリングポインタの可能性
- スレッド安全性の保証なし
だから FFI 呼び出しは unsafe ブロックが必要。
extern ブロック
基本形
extern "C" {
fn function_name(arg1: Type1, arg2: Type2) -> ReturnType;
}
-
"C": 呼び出し規約(ほとんどの場合これ) - 関数のシグネチャを宣言
呼び出し規約
extern "C" { } // C 言語の呼び出し規約(デフォルト)
extern "system" { } // OS のシステム呼び出し規約(Windows API用)
extern "stdcall" { } // Windows 32bit API
可変長引数
extern "C" {
fn printf(format: *const i8, ...) -> i32;
}
型のマッピング
基本型の対応
| C言語 | Rust | libc |
|---|---|---|
int |
i32 |
c_int |
unsigned int |
u32 |
c_uint |
long |
i64 (LP64) |
c_long |
unsigned long |
u64 (LP64) |
c_ulong |
char |
i8 |
c_char |
unsigned char |
u8 |
c_uchar |
float |
f32 |
c_float |
double |
f64 |
c_double |
void |
() |
c_void |
size_t |
usize |
size_t |
ポインタ型
// const char* → *const c_char
// char* → *mut c_char
// void* → *mut c_void
// const void* → *const c_void
use std::os::raw::{c_char, c_void};
extern "C" {
fn strlen(s: *const c_char) -> usize;
fn malloc(size: usize) -> *mut c_void;
fn free(ptr: *mut c_void);
}
構造体
// C言語
struct Point {
int x;
int y;
};
// Rust
#[repr(C)]
struct Point {
x: i32,
y: i32,
}
#[repr(C)] で C言語と同じメモリレイアウトを保証。
列挙型
// C言語
enum Color {
RED = 0,
GREEN = 1,
BLUE = 2
};
// Rust
#[repr(C)]
enum Color {
Red = 0,
Green = 1,
Blue = 2,
}
実践:libc を使う
セットアップ
[dependencies]
libc = "0.2"
getpid を呼ぶ
use libc::{getpid, pid_t};
fn main() {
let pid: pid_t = unsafe { getpid() };
println!("PID: {}", pid);
}
文字列の扱い
Rust → C
use std::ffi::CString;
use libc::{c_char, puts};
fn main() {
let rust_string = "Hello from Rust!";
let c_string = CString::new(rust_string).unwrap();
unsafe {
puts(c_string.as_ptr());
}
}
C → Rust
use std::ffi::CStr;
use libc::c_char;
extern "C" {
fn getenv(name: *const c_char) -> *const c_char;
}
fn main() {
let key = CString::new("PATH").unwrap();
unsafe {
let value_ptr = getenv(key.as_ptr());
if !value_ptr.is_null() {
let value = CStr::from_ptr(value_ptr);
println!("PATH: {:?}", value.to_str().unwrap());
}
}
}
メモリ操作
use libc::{malloc, free, c_void, size_t};
use std::ptr;
fn main() {
unsafe {
// メモリ確保
let size: size_t = 100;
let ptr = malloc(size);
if ptr.is_null() {
panic!("malloc failed");
}
// 使用
let slice = std::slice::from_raw_parts_mut(ptr as *mut u8, size);
slice[0] = 42;
// 解放
free(ptr);
}
}
安全なラッパーを作る
FFI を直接使うのは危険。安全なラッパーを作ろう。
パターン1: Option で null チェック
use std::ffi::{CStr, CString};
use libc::c_char;
extern "C" {
fn getenv(name: *const c_char) -> *const c_char;
}
// 安全なラッパー
fn safe_getenv(key: &str) -> Option<String> {
let c_key = CString::new(key).ok()?;
unsafe {
let ptr = getenv(c_key.as_ptr());
if ptr.is_null() {
None
} else {
CStr::from_ptr(ptr)
.to_str()
.ok()
.map(|s| s.to_string())
}
}
}
fn main() {
match safe_getenv("PATH") {
Some(path) => println!("PATH: {}", path),
None => println!("PATH not set"),
}
}
パターン2: Result でエラーハンドリング
use std::ffi::CString;
use std::io;
use libc::{c_char, c_int, open, close, O_RDONLY};
// 安全なラッパー
fn safe_open(path: &str) -> io::Result<i32> {
let c_path = CString::new(path)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid path"))?;
let fd = unsafe { open(c_path.as_ptr(), O_RDONLY) };
if fd < 0 {
Err(io::Error::last_os_error())
} else {
Ok(fd)
}
}
fn safe_close(fd: i32) -> io::Result<()> {
let result = unsafe { close(fd) };
if result < 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
fn main() -> io::Result<()> {
let fd = safe_open("/etc/passwd")?;
println!("Opened fd: {}", fd);
safe_close(fd)?;
Ok(())
}
パターン3: RAII でリソース管理
use std::ffi::CString;
use std::io;
use libc::{c_int, open, close, read, O_RDONLY};
struct File {
fd: c_int,
}
impl File {
fn open(path: &str) -> io::Result<Self> {
let c_path = CString::new(path)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "Invalid path"))?;
let fd = unsafe { open(c_path.as_ptr(), O_RDONLY) };
if fd < 0 {
Err(io::Error::last_os_error())
} else {
Ok(File { fd })
}
}
fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
let n = unsafe {
read(self.fd, buf.as_mut_ptr() as *mut _, buf.len())
};
if n < 0 {
Err(io::Error::last_os_error())
} else {
Ok(n as usize)
}
}
}
impl Drop for File {
fn drop(&mut self) {
unsafe {
close(self.fd);
}
}
}
fn main() -> io::Result<()> {
let file = File::open("/etc/passwd")?;
let mut buf = [0u8; 100];
let n = file.read(&mut buf)?;
println!("Read {} bytes", n);
// file は自動的に close される
Ok(())
}
bindgen
C のヘッダファイルから Rust のバインディングを自動生成!
インストール
cargo install bindgen-cli
使い方
// example.h
typedef struct {
int x;
int y;
} Point;
Point create_point(int x, int y);
int distance(Point* a, Point* b);
bindgen example.h -o bindings.rs
生成される bindings.rs:
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct Point {
pub x: ::std::os::raw::c_int,
pub y: ::std::os::raw::c_int,
}
extern "C" {
pub fn create_point(
x: ::std::os::raw::c_int,
y: ::std::os::raw::c_int,
) -> Point;
}
extern "C" {
pub fn distance(
a: *mut Point,
b: *mut Point,
) -> ::std::os::raw::c_int;
}
build.rs で自動化
# Cargo.toml
[build-dependencies]
bindgen = "0.69"
// build.rs
use std::env;
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-changed=wrapper.h");
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}
// src/lib.rs
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
よくある落とし穴
1. 文字列の寿命
// ❌ 危険!CString がスコープを抜けると解放される
fn bad() -> *const c_char {
let s = CString::new("hello").unwrap();
s.as_ptr() // ダングリングポインタ!
}
// ✅ 安全
fn good() {
let s = CString::new("hello").unwrap();
unsafe {
some_c_function(s.as_ptr()); // s はまだ生きている
}
} // ここで s が解放される
2. NULL ポインタチェック
// ❌ NULL チェックなし
unsafe {
let ptr = some_c_function();
let value = *ptr; // NULL なら Undefined Behavior
}
// ✅ NULL チェックあり
unsafe {
let ptr = some_c_function();
if !ptr.is_null() {
let value = *ptr;
}
}
3. メモリの所有権
// C で確保したメモリは C で解放
unsafe {
let ptr = malloc(100);
// ... 使用 ...
free(ptr); // malloc したら free
}
// Rust で確保したメモリは Rust で解放
let boxed = Box::new(42);
let ptr = Box::into_raw(boxed);
unsafe {
// C に渡す
some_c_function(ptr);
// 使い終わったら Rust で解放
let _ = Box::from_raw(ptr);
}
まとめ
FFI のベストプラクティス
- 最小限の unsafe - unsafe ブロックは小さく
- 安全なラッパー - 公開 API は safe に
- NULL チェック - 常に行う
- エラーハンドリング - Result/Option を活用
- RAII - リソース管理を自動化
- bindgen - 自動生成を活用
チェックリスト
-
#[repr(C)]を構造体に付けたか - NULL ポインタをチェックしたか
- 文字列の寿命は適切か
- メモリの所有権は明確か
- エラーハンドリングは適切か
参考リンク
FFI は unsafe の世界だけど、適切にラップすれば安全に使えます。C資産を活かしながら、Rust の安全性も享受しましょう!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!