オフィスアワーがそろそろ始まるよ!()

Cとのインタフェース

Zen言語はC言語との相互運用性を重要視しています。なぜならば、既存のC資産を活用しながら、Zen言語の導入を少しずつ進めていけるようにするためです。その一方、Zen言語はC言語に依存していません (他の多くのプログラミング言語がlibcに依存している一方、Zen言語でlibcをリンクするのはオプションです) 。Zen言語とC言語とは、あくまでも別の言語であるため、その相互運用には若干の橋渡しが必要です。

ここでは、ZenとCとを相互運用するためのインタフェースについて説明します。

C互換型

C言語とのABIレベルでの互換性を確保するための型は、以下の通りです。

型名 C言語で等価の型
c_short short
c_ushort unsigned short
c_int int
c_uint unsigned int
c_long log
c_ulong unsigned long
c_longlong long long
c_ulonglong unsigned long long
c_longdouble long double
c_void void

その他のZenのプリミティブ型と、C言語 (C99以降) との対応は以下の通りです。

型名 C言語で等価の型
i8 int8_t
u8 uint8_t
i16 int16_t
u16 uint16_t
i32 int32_t
u32 uint32_t
i64 int64_t
u64 uint64_t
i128 __int128
u128 unsigned __int128
isize intptr_t
usize uintptr_t
f16 _Float16
f32 float
f64 double
f128 _Float128
bool bool

文字列の相互運用

C言語とZenとでは文字列の扱い方が異なります。そのため、文字列を操作するC言語の関数に、Zenから文字列を引数として渡す場合には、C言語の作法に則った文字列を渡します。

C言語の文字列はNULL文字 (\0) で終端された配列です。一方、Zenでの文字列は、u8の配列またはスライスで、文字列の終端は配列またはスライスのサイズで判断します。したがって、C言語に文字列を渡す場合には、NULL文字を末尾に付ける必要があります。

ZenでNULL文字終端された文字列を作成する方法は2つあります。

  1. NULL文字終端された文字列リテラルを使用する
  2. 新たにメモリを確保し、文字列の末尾にNULL文字を追加する

注意: C言語の文字列を利用することは、安全ではないため、C言語とのインタフェース以外では利用しないことをお勧めします。なぜなら、C言語の文字列は、その文字列が格納されている配列の要素数情報を持っていないためです。これは容易にバッファオーバーフローを発生させる原因になります。一方、Zenの文字列は、配列にせよスライスにせよ要素数情報を保有しており、バッファオーバーフローが生じることはありません。C言語から文字列を受け取った場合、その長さがわかるのであれば、速やかにスライスへと変換すべきです。

NULL文字終端された文字列リテラルを使用する

文字列リテラルの先頭にcを付けると、文字列リテラルはNULL文字で終端されます。複数行にまたがるC言語文字列リテラルを作成する場合は、c\\を行頭に付けます。

examples/ch14-c/c_str/src/c_str.zen:6:9

    const c_str = c"this is a c string";
    const multiline = c\\this is 
                      c\\multiline c string
                    ;

この文字列リテラルを格納した変数c_strの型は、配列でもスライスでもありません。[*]const u8という要素数が不明な配列へのポインタになります。Zenの文字列リテラルとの型の違いは次のテストコードで確かめることができます。

examples/ch14-c/c_str/src/c_str.zen:4:12

test "cstr literal" {
    const zen_str = "this is a Zen string";
    const c_str = c"this is a c string";
    const multiline = c\\this is 
                      c\\multiline c string
                    ;
    ok(@typeOf(zen_str) == [20]u8);
    ok(@typeOf(c_str) == [*]const u8);
}

上記のc_strはC言語側で正常に取り扱うことが可能です。一方、zen_strをNULL文字終端された文字列を期待しているC言語関数に渡すと、多くの場合バッファオーバーフローが発生するでしょう。

捕捉: バッファオーバーフローは確保しているメモリの範囲を越えて、メモリアクセスすることです。上記の例では、zen_strは要素数20の配列ですが、その要素数を越えたメモリにアクセスすることを意味します。

後ほど、C言語の関数を呼び出す方法を詳しく説明しますが、C言語の標準ライブラリ関数putsを利用して、2種類の文字列を表示するコードは、次の通りです。

extern fn puts([*]const u8) void;

test "cstr literal" {
    const zen_str = "this is a Zen string";
    const c_str = c"this is a c string";

    // output: this is a c string
    puts(c_str);
    // 未定義動作。多くの場合"this is a Zen string"の後に
    // ゴミデータが出力される
    puts(&zen_str);
}

このコードは、次のコマンドで実行できます。

$ zen test c_str.zen --library c

著者の環境では、次の出力になりました。Zenの文字列に続いて、test failureという文字列が出力されています (未定義動作であるため、必ずしも同じ状況が再現できるとは限りません) 。

this is a c string
this is a Zen stringtest failure

これはputs関数がNULL文字を見つけるまで文字を出力するためです。zen_strが本来確保しているメモリの範囲を越えて、文字列を表示しており、バッファオーバーフローが発生しています。

新たにメモリを確保し、文字列の末尾にNULL文字を追加する

標準ライブラリにはC言語の文字列を扱うための関数がstd.cstrに用意されています。addNullByte関数を使用すると、Zenの文字列の末尾にNULL文字を追加した文字列を得ることができます。addNullByteの第一引数はメモリアロケータで、第二引数はZenの文字列です。

examples/ch14-c/c_str/src/c_str.zen:14:20

test "addNullByte" {
    var buf: [100]u8 = undefined;
    const allocator = &std.heap.FixedBufferAllocator {.buffer = &buf };

    const zen_str = "this is a Zen string";
    const c_str = try std.cstr.addNullByte(allocator, &zen_str);
}

addNullByteは、新しくメモリ領域を確保し、第二引数で与えられた文字列の末尾にNULL文字を追加した上で確保した領域に文字列をコピーし、そのコピーした文字列スライスを返します。メモリアロケーションに失敗した場合、error.OutOfMemoryが返ります。

addNullByteがの戻り値はスライスであるため、C言語の関数に渡す際は、スライスの.ptrフィールドを渡します。

extern fn puts([*]const u8) void;

test "addNullByte" {
    var buf: [100]u8 = undefined;
    const allocator = &std.heap.FixedBufferAllocator.init(&buf).allocator;

    const zen_str = "this is a Zen string";
    const c_str = try std.cstr.addNullByte(allocator, &zen_str);
    puts(c_str.ptr);
}

c_strはNULL文字で終端されているため、C言語のputs関数でも正しく扱うことができます。

$ zen test c_str.zen --library c
this is a Zen string

ZenからCを呼ぶ

ZenのプロジェクトでC言語のソースコードやライブラリを使用する方法を学びます。3つの方法があります。

  1. CソースファイルをZenソースファイルに変換する
  2. 手動でCインタフェースを作成する
  3. Cヘッダファイルからインポートする

CソースファイルをZenソースファイルに変換する

CソースファイルをZenソースファイルに変換し、プロジェクト全体をZenとしてビルドする方法です。Cソースファイルが手元にあり、そのソースファイルのビルドが複雑でないときに便利な方法です。

CソースファイルをZenソースファイルに変換するには、Zenコンパイラのtranslate-cコマンドを使用します。

次のようなadd.cがあるとします。

examples/ch14-c/translate-c/add.c:1:3

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

これをtranslate-cadd.zenに変換します。

$ zen translate-c add.c > add.zen

add.zenの先頭は、add関数をZen言語に変換した結果です。残りはCのdefineで定義された定数が続いています。

examples/ch14-c/translate-c/add.zen:1:3

pub export fn add(a: c_int, b: c_int) c_int {
    return a + b;
}

後は、Zenでadd.zenをインポートして利用するだけです。次のようなuse_add.zenadd.zenと同じディレクトリに作成します。

examples/ch14-c/translate-c/use_add.zen:1:7

const std = @import("std");
const ok = std.testing.ok;
const c = @import("add.zen");

test "use translated C function" {
    ok(c.add(1, 2) == 3);
}
$ zen test use_add.zen 
1/1 test "use translated C function"...OK
All tests passed.

手動でCインタフェースを作成する

externを使い手動でCインタフェースを作成します。C言語で作られたライブラリを利用する際に使用できる手段です。ただし、使用するC言語の関数や構造体が多い場合は、後述するCヘッダファイルをインポートする方法を使う方が効率的です。

次のようなadd.cadd.hがあるとします。

// add.h
int add(int a, int b);
// add.c
int add(int a, int b) {
    return a + b;
}

add.cはビルド済みのライブラリlibadd.aを使用します。下準備としてZenコンパイラで次のコマンドを実行してライブラリファイルを作成します。

$ zen cc -c add.c
$ zen build-lib --object add.o --name add

add関数はint add(int a, int b);とプロトタイプ宣言されているため、Zenのコード内でadd関数を宣言します。C言語の (ABIを持つ) 関数を宣言するにはexternキーワードを使います。関数を宣言した後は、通常のZenの関数と同じように呼び出せます。use_libadd.zenを次の内容で作成します。

examples/ch14-c/extern-c-interface/use_libadd.zen:1:8

const std = @import("std");
const ok = std.testing.ok;

extern fn add(a: c_int, b: c_int) c_int;

test "use libadd.a" {
    ok(add(1, 2) == 3);
}

use_libadd.zenlibadd.aと一緒にビルドします。libadd.aをライブラリとしてリンクするために、--library add -L.をZenコンパイラへのオプションとして指定します。

$ zen test use_libadd.zen --library add -L.
1/1 test "use libadd.a"...OK
All tests passed.

無事、C言語のライブラリlibadd.aの関数が利用できました。

add.htranclate-cにかけることでC言語の関数宣言を自動生成することも可能です。add.htranslate-cにかけると、次のZenソースコードを出力します。

pub extern fn add(a: c_int, b: c_int) c_int;

次のコマンドでadd_h.zenを自動生成し、

$ zen translate-c add.h > add_h.zen

add_h.zenをインポートすれば、libadd.aを利用することができます。


const c = @import("add_h.zen");
test "use libadd.a using translate-c" {
    ok(c.add(1, 2) == 3);
}

Cヘッダファイルからインポートする

Zenではもっと簡単にC言語のライブラリを使用することができます。それは、組込み関数の@cImportを使ってZenのソースコード内で直接Cヘッダファイルをインポートする方法です。

C言語標準ライブラリのprintfを呼び出すコード (import_c.zen) を示します。

examples/ch14-c/import-c/import_c.zen:1:7

const c = @cImport ({
    @cInclude("stdio.h");
});

pub fn main() void {
    _ = c.printf(c"hello\n");
}

このコードは次のコマンドで実行できます。

$ zen run import_c.zen --library c
hello

@cImportは引数に1つの式 ({}も1つの式です) を取ります。この式はコンパイル時に実行されます。@cImportの中では、組込み関数の@cInclude / @cDefine / @cUndefが使えます。

注意: @cInclude / @cDefine / @cUndef@cImportの中でしか使用できません。

@cIcludeはCヘッダファイルを#include <ヘッダファイル>として読み込みます。そのため、ヘッダファイルはインクルードパスから辿れる場所に置かれている必要があります。ビルド時にインクルードパスを追加することで、任意ディレクトリ内のヘッダファイルをインクルードできます。

@cDefineはプリプロセッサマクロを定義し、@cUndefはプリプロセッサマクロの定義を削除します。

もう少し複雑な例を示します。

examples/ch14-c/import-c/complex_import.zen:1:29

const std = @import("std");
const builtin = @import("builtin");

const c = @cImport ({
    // 複数のヘッダファイルをインクルード可能
    @cInclude("stdio.h");
    @cInclude("stdlib.h");

    // マクロの定義
    @cDefine("ZEN", "");
    // コンパイル時計算可能な値を使った分岐
    if (builtin.arch == .x86_64) {
        // 文字列を定義する場合は、エスケープする
        @cDefine("ARCH", "\"x86_64\"");
    }
    // 次のような書き方も可能
    @cDefine("DEBUG", if (builtin.mode == .Debug) "1" else "0");

    // マクロ定義の削除
    @cUndef("ZEN");
});

pub fn main() void {
    std.debug.assert(c.DEBUG == 1);
    // C言語の文字列比較
    const result = std.cstr.cmp(c.ARCH, c"x86_64");
    std.debug.assert(result == 0);
    _ = c.printf(c"hello\n");
}

CからZenを呼ぶ

次は、Zenの関数をC言語から呼び出す方法です。手順は2つです。

  1. ZenにC言語のためのインタフェースを用意する
  2. ZenプロジェクトをC言語のビルドシステムに組み込む

1.は、C言語ABIを満たす関数およびデータ構造をZenで作成し、Cヘッダファイルを作成します。2.は、C言語ビルドシステムに何を使用しているか (例えば、MakefileやCMake) によって具体的な方法は異なってきます。ここでは、事前に用意したZenライブラリを、Cコンパイラ (clang) でビルドする方法を説明します。

ZenにC言語のためのインタフェースを用意する

ZenとC言語とではABIが異なるため、C ABIを満たすZenのコードを書かなければなりません。幸い、それほど難しくありません。関数であればexportまたはpub extern "c"を付けて宣言します。構造体や列挙型であれば、externキーワードを付けます。

point.zenに2次元の座標を表すPoint構造体を定義します。これまで見てきた構造体と異なる点は、structの前にexternキーワードがあることです。このexternを付けることで、C言語とABIレベルでの互換性が保証されます。

examples/ch14-c/export/point.zen:1:4}

pub const Point = extern struct {
    x: f64,
    y: f64,
};

次に、Pointのポインタを受け取り、フィールドの値を2倍にするdoublePointpoint.zenに定義します。ここではexportが先頭についています。これでC言語とABIの互換性が保証されます。

examples/ch14-c/export/point.zen:6:9

export fn doublePoint(point: *Point) void {
    point.x *= 2;
    point.y *= 2;
}

なお、これはextern "c"でC ABIを指定した関数を、組込み関数の@exportでグローバルリンケージを指定した以下のコードと等価です。

extern "c" fn doublePoint(point: *Point) void {
    point.x *= 2;
    point.y *= 2;
}

comptime {
    const builtin = @import("builtin");
    @export("doublePoint", doublePoint, builtin.GlobalLinkage.Strong);
}

ではこのPoint構造体とdoublePoint関数を含むCヘッダファイルを作成します。これはZenコンパイラでpoint.zenをライブラリとしてビルドするだけで自動生成できます。次のコマンドを実行してみて下さい。

$ zen build-lib point.zen

ターゲットのライブラリであるlibpoint.aだけでなく、point.hが生成されていることがわかります。

$ ls
libpoint.a  point.h  point.o  point.zen

このpoint.hの中身を確認すると、Point構造体とdoublePoint関数が宣言されています。

#ifdef __cplusplus
#define POINT_EXTERN_C extern "C"
#else
#define POINT_EXTERN_C
#endif
// ...
struct Point {
    double x;
    double y;
};

// POINT_EXTERN_CはC++からもincludeできるようにするためのマクロ
POINT_EXTERN_C void doublePoint(struct Point * point);

これで、C言語ABIを満たす関数およびデータ構造をZenで作成し、Cヘッダファイルを作成しました。

ZenプロジェクトをC言語のビルドシステムに組み込む

C言語のコンパイラであるclangを使用して、libpoint.aを使用するCプログラムを作成します。次のように、Zenコンパイラが自動生成したpoint.hをCソースファイルでインクルードし、Point構造体やdoublePointを通常通り使用します。

examples/ch14-c/export/main.c:1:10

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

int main(void) {
    struct Point p = { 1.0, 2.0 };
    doublePoint(&p);

    printf("x = %lf, y = %lf\n", p.x, p.y);
    return 0;
}

このコードをビルドして実行してみましょう。pの初期値はxに1.0、yに2.0を与えているので、doublePoint呼び出し後にそれぞれ、2.0、4.0になっていれば期待通り動作しています。

$ clang main.c libpoint.a
$ ./a.out 
x = 2.000000, y = 4.000000

ポインタの相互運用

C言語との相互運用を行う上で、ポインタの扱いには気をつけなければなりません。特にtranslate-c@cIncludeでZenの関数宣言を自動生成した場合は、ポインタを慎重に取り扱わなければなりません。

Zenのポインタと異なり、C言語のポインタは単一オブジェクトを指しているか、配列を指しているか、が曖昧です。例えば、単一のcharを指すポインタも、charの配列を指すポインタも、どちらも*charになります。あるC言語の関数の引数または戻り値が単一オブジェクトを指すか配列を指すか、はプログラマが判断しなければなりません。

translate-c@cIncludeを使用する場合、ZenコンパイラはCポインタが単一オブジェクトであるか、配列を指すか、がわかりません。そこで、ZenコンパイラはC言語のポインタを表現するためのCポインタ型 ([*c]T) を使用します。

例えば、C言語標準ライブラリのfprintf関数は、次のように宣言されています。第一引数は単一のFILE構造体オブジェクトへのポインタ、第二引数は文字列へのポインタです。

int fprintf(FILE *__stream, const char *__format, ...);

fprintfに対するZenの関数宣言を手動で作成する場合、単一オブジェクトへのポインタと配列へのポインタを区別し、次のように宣言すべきです。

pub extern fn fprintf(__stream: *FILE, __format: [*]const u8, ...) c_int;

しかし、Zenコンパイラが自動生成するfprintfの宣言は以下のようになります。

pub extern fn fprintf(__stream: [*c]FILE, __format: [*c]const u8, ...) c_int;

[*c]T (Tは任意の型) はCポインタ型です。このCポインタ型は次の特徴を持ちます。

  • 他のポインタ型と同じ操作 (.*によるデリファレンス、[]による要素アクセス、算術演算) が可能。
  • 整数型との比較演算が可能。
  • Zenのポインタ型およびオプション型で包まれたポインタへ暗黙の型変換可能。オプション型で包まれていない場合、0番地へのアクセスは安全性保護付き未定義動作を起こす。
  • 整数型からの暗黙の型変換
  • 0番地を格納可能。OS上でプログラムが動作している場合、0番地へのアクセスは安全性保護付き未定義動作を起こす。

examples/ch14-c/pointer/src/c_pointer.zen:4:33

test "c pointer" {
    const buf = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    const ptr: [*c]u8 = &buf;

    // デリファレンス
    ok(ptr.* == 0);
    // 要素アクセス
    ok(ptr[1] == 1);
    // 算術演算
    ok((ptr + 1).* == 1);
    // 整数との比較
    ok(ptr != 0);

    // オプショナル型に包まれたポインタへの暗黙の型変換
    const optional: ?*u8 = ptr;
    if (optional) |p| {
        ok(p.* == 0);
    }

    // 整数型からの暗黙の型変換
    const addr: [*c]u8 = 0xDEADBEEF;
    // Compile error: expected '*u8' type, found 'comptime_int'
    // @intToPtrを使った明示的な型変換が必要
    // const cannot_cast: *u8 = 0xDEADBEEF;

    // `0`番地を格納可能
    const zero: [*c]u8 = 0;
    // Compile error: pointer type '*u8' does not allow address zero
    // const not_allow_zero: *u8 = @intToPtr(*u8, 0);
}

このようにZenの他のポインタ型と比較して、Cポインタ型は危険な操作が簡単にできてしまいます。Cポインタ型は速やかに他のポインタ型に変換すべきです。

注意: Cポインタ型は、Cとのインタフェースを自動生成する時以外では利用してはなりません。Zenが提供する様々な安全性チェックは、Cポインタ型に対してはほとんど行われません

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2019 connectFree Corporation. All rights reserved.
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.