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

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

文字列の相互運用

Zenの文字列はC言語の文字列と同様NULL文字 (\0) で終端したバイト列となっています。そのため、ZenからC言語へ文字列を引数として渡す場合にはそのまま渡すことが可能です。

Zen の文字列については、2.8 文字列で詳しく説明しています。

Zenでは文字列リテラルを宣言するとNULL文字終端された文字列が作成されますが、NULL文字終端されていないバイト列の末尾にNULL文字を追加する方法も用意されています。

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

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

fn addNullByte(allocator: *mut vtable heap.Allocator, slice: []u8) ![:0]mut u8

addNullByteは、新しくメモリ領域を確保し、第二引数で与えられたバイト列の末尾にNULL文字を追加した上で確保した領域に文字列をコピーし、そのコピーした文字列スライスを返します。 メモリアロケーションに失敗した場合、error.OutOfMemoryが返ります。 文字列スライスは要素数情報を保有しているためバッファオーバーフローを発生させることなく安全に使用することができます。

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

examples/ch14-c/c_str/src/c_str.zen:13:25

extern fn puts([*]u8) void;

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

    const array = [_]u8{ 'h', 'e', 'l', 'l', 'o' };
    const str = try std.cstr.addNullByte(&mut allocator, &array);
    ok(@TypeOf(str) == [:0]mut u8);
    ok(str.len == 5);

    puts(str.ptr);
}

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

$ zen test c_str.zen --library c
hello

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(arg_a: c_int, arg_b: c_int) c_int {
    var a = arg_a;
    var b = arg_b;
    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 を作成し、これを後で use_libadd.zen から使用します。 下準備として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.htranslate-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("hello\n");
}

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

$ zen run import_c.zen --library c
hello

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

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

@cIncludeは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, "x86_64");
    std.debug.assert(result == 0);
    _ = c.printf("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: *mut Point) void {
    point.x *= 2;
    point.y *= 2;
}

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

extern "c" fn doublePoint(point: *mut 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関数が宣言されています。

#ifndef POINT_H
#define POINT_H


struct Point {
    double x;
    double y;
};

#ifdef __cplusplus
extern "C" {
#endif

void doublePoint(struct Point * point);
void __zen_probe_stack(void);

#ifdef __cplusplus
} // extern "C"
#endif


#endif // POINT_H

これで、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: *mut FILE, __format: [*]u8, ...) c_int;

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

pub extern fn fprintf(__stream: [*c]mut FILE, __format: [*c]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" {
    var 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ポインタ型に対してはほとんど行われません

Chapter 1

Chapter 2

Chapter 3

Chapter 4

Chapter 5

Chapter 6

Chapter 7

Chapter 8

Chapter 9

Chapter 10

Chapter 11

Chapter 12

Chapter 13

Chapter 14

Chapter 15

Appendix

Error Explanation

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2020 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.