Zen言語はC言語との相互運用性を重要視しています。なぜならば、既存のC資産を活用しながら、Zen言語の導入を少しずつ進めていけるようにするためです。その一方、Zen言語はC言語に依存していません (他の多くのプログラミング言語がlibc
に依存している一方、Zen言語でlibc
をリンクするのはオプションです) 。Zen言語とC言語とは、あくまでも別の言語であるため、その相互運用には若干の橋渡しが必要です。
ここでは、Zenと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文字を追加する方法も用意されています。
標準ライブラリには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言語のソースコードやライブラリを使用する方法を学びます。3つの方法があります。
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-c
でadd.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.zen
をadd.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.
extern
を使い手動でCインタフェースを作成します。C言語で作られたライブラリを利用する際に使用できる手段です。ただし、使用するC言語の関数や構造体が多い場合は、後述するCヘッダファイルをインポートする方法を使う方が効率的です。
次のようなadd.c
とadd.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.zen
をlibadd.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.h
をtranslate-c
にかけることでC言語の関数宣言を自動生成することも可能です。add.h
をtranslate-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);
}
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");
}
次は、Zenの関数をC言語から呼び出す方法です。手順は2つです。
1.は、C言語ABIを満たす関数およびデータ構造をZenで作成し、Cヘッダファイルを作成します。2.は、C言語ビルドシステムに何を使用しているか (例えば、MakefileやCMake) によって具体的な方法は異なってきます。ここでは、事前に用意したZenライブラリを、Cコンパイラ (clang) でビルドする方法を説明します。
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倍にするdoublePoint
をpoint.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ヘッダファイルを作成しました。
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ポインタ型は次の特徴を持ちます。
.*
によるデリファレンス、[]
による要素アクセス、算術演算) が可能。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ポインタ型に対してはほとんど行われません。
☰ 人の生きた証は永遠に残るよう ☰
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.