C の char は signed/unsigned が実装依存だから Rust から呼び出すときは c_char を使おう

C の char は signed/unsigned が実装依存だから Rust から呼び出すときは c_char を使おう

前提 #

所与の条件として、下記の C/C++ の関数が存在するとします。

ヘッダーファイル(C および C++ 両対応):hello.h #

#pragma once

#ifdef __cplusplus
extern "C" {
#endif

void hello(const char *text);

#ifdef __cplusplus
}
#endif

実装ファイル:hello.c または hello.cpp #

#include <stdio.h>
#include "hello.h"

void hello(const char *text) {
    printf("%s\n", text);
}

コンパイル #

C としてコンパイルする場合

gcc -c hello.c -o hello.o

C++ としてコンパイルする場合

g++ -c hello.cpp -o hello.o

Rust から呼び出すコードを書いてみる #

さて、コンパイルされた C/C++ ライブラリを呼び出す Rust のコードを書いてみましょう。

main.rs

use std::ffi::CString;

unsafe extern "C" {
    fn hello(text: *const char);
}

fn main() {
    let text = CString::new("hello").unwrap();

    unsafe {
        hello(text.as_ptr());
    }
}

パッと上記が思いつきそうですが、これは間違っています。なぜなら、C の char 型は Rust の char 型とは異なるためです。名称は同じながら別の型です。

C の char は 1 バイトの整数型ですが、Rust の char は Unicode 文字を表す型です。両者に互換性はありません。

では C の char に対応する Rust の型は何でしょうか。C の char は 1 バイトの整数型なので、Rust の i8 または u8 が思い浮かぶかもしれません。

use std::ffi::CString;

unsafe extern "C" {
    fn hello(text: *const i8);
    // または fn hello(text: *const u8);
}

fn main() {
    let text = CString::new("hello").unwrap();

    unsafe {
        hello(text.as_ptr());
    }
}

ここで、Rust の i8 は符号付き整数、u8 は符号なし整数です。

では、C の char はどちらでしょうか?実は、C の plain char が符号付きか符号なしのどちらかで扱われるかは実装依存(implementation-defined)です。つまり、環境によって異なります。

Which of signed char or unsigned char has the same range, representation, and behavior as “plain” char.

Determined by ABI.

https://gcc.gnu.org/onlinedocs/gcc/Characters-implementation.html

「Determined by ABI」の ABI は Application Binary Interface(アプリケーション・バイナリ・インターフェース)の略です。要するにコンパイル時に指定するターゲット環境(ターゲットトリプル)によって、C の char が符号付きか符号なしが分かれるということです。

より具体的に、よく使われるであろう Rust のターゲットトリプルを例にあげると、x86_64-unknown-linux-gnu では C の char は符号付きの signed char であり、aarch64-unknown-linux-gnu では C の char は符号なしの unsigned char になります。

従って、text.as_ptr()*const i8 を返すのか *const u8 を返すのかは、ターゲット環境によって異なります。

text.as_ptr()*const u8 を返す環境の場合、extern ブロックの関数宣言が fn hello(text: *const i8); となっていると型が合いません。逆に、text.as_ptr()*const i8 を返す環境の場合、extern ブロックの関数宣言が fn hello(text: *const u8); となっていると、同様に方が合いません。その結果、cargo checkcargo build 時にコンパイルエラーになります。

c_char を使おう #

ということで、ターゲット環境によって C の char が符号付きか符号なしのどちらになるかを気にせずに、Rust から C の char を扱うための型が用意されています。それが c_char です。

c_char はターゲット環境に応じて i8 または u8 のどちらかに切り替わる型エイリアスです。

use std::ffi::c_char;
use std::ffi::CString;

unsafe extern "C" {
    fn hello(text: *const c_char);
}

fn main() {
    let text = CString::new("hello").unwrap();

    unsafe {
        hello(text.as_ptr());
    }
}

リンキングと実行 #

C/C++ のオブジェクトファイルとリンクして、Rust の実行ファイルを作成する。

rustc --edition 2024 main.rs -C link-arg=hello.o

実行する。

./main