😊

既存のC言語による実装をインクリメンタルにZigに移植する

2023/02/21に公開

興味深い記事を見つけました。
気に入ったところだけピックアップして紹介するので、ぜひ原文も読んでください。

https://www.ringtailsoftware.co.uk/zig-rv32/

https://twitter.com/tetsu_koba/status/1627667263343181825?conversation=none
https://twitter.com/tetsu_koba/status/1627670631436398595?conversation=none

Zig is an ideal language for incrementally porting existing C code to as I’ll hopefully show.

Zigは既存のCのコードをインクリメンタルに(少しずつ)移植するのに理想的な言語です。これからそれを紹介します。

Mini-RV32ima

Mini-RV32imaはひとつのCのヘッダファイルからなるRISC-Vのエミュレータです。(このエミュレータでLinuxカーネルを起動することが可能です)
私(元のブログの筆者)はRISC-Vのことをあまり知りません。RISC-Vのリファレンスを見るよりもCのコードを一行ずつ忠実にZigに書き直しました。

最初の一歩

zig はCコンパイラとして使うこともできます。最初にやったことは、CのコードをまるごとZigから呼び出すことでした。Cのmain関数を_mainに変更して、以下のようにしました。

Zig:

const rv32 = @cImport({
    @cInclude("mini-rv32ima.h");
}
pub fn main() !void {
    rv32.doEverything();
}

C:

void doEverything(void) {
    char *argv[] = {
        "minirv32", "-f", "Image"
    };
    _main(3, argv);
}

int _main(int argc, char *argv[]) {
    ...
}

ZigではビルドシステムをZig自身で記述します。build.zigはこのようになります。

build.zig
const rv32ima_flags = [_][]const u8{
    "-Wall",
};

const rv32ima_sources = [_][]const u8{
    "mini-rv32ima/mini-rv32ima.c",
};

fn createrv32ima(b: *std.build.Builder) *std.build.LibExeObjStep {
    const lib = b.addStaticLibrary("rv32ima", null);
    lib.addCSourceFiles(&rv32ima_sources, &rv32ima_flags);
    lib.addIncludePath("mini-rv32ima");
    lib.linkSystemLibraryName("c");
    return lib;
}

関数ごとに書き直していく

関数HandleControlStoreはエミュレータが動いたときにMemory mapped I/O への書き込みを行います。addressが0x10000000の場合はそのキャラクタをターミナルに出力します。

C:

uint32_t HandleControlStore( uint32_t addy, uint32_t val ) {
    if( addy == 0x10000000 ) { //UART 8250 / 16550 Data Buffer
        printf( "%c", val );
        fflush( stdout );
    }
    return 0;
}

Zig:

export fn HandleControlStore(addr:u32, val:u32) callconv(.C) u32 {
    if (addr == 0x10000000 ) { //UART 8250 / 16550 Data Buffer
        std.debug.print("{c}", .{@intCast(u8, val)});
    }
    return 0;
}

Zigではexportcallconv(.C)を追加するだけです。
(訳者注: exportをつけるとデフォルトでC ABIになるので、callconv(.C)は省略可能だと思います。)

(中略)他にもいろいろ書いてありますがスキップします。ぜひ原文をみてください。

WASM

Zigはwasmをターゲットとしてコード出力することができます。全てをZigに移植するまでもなく、Cのコードをwasmにトランスレートしてブラウザの上で動かすことができます。このためには全てのlibcへの依存を除去する必要があります。でもこれは今回はとても簡単でした。すでにI/Oのハンドリングの部分をZigに置き換えていたからです。
Cと同様に、関数をJavaScriptからアクセス可能にするのはシンプルです。

Zig:

export fn tty_read(b:u8) void {
    // handle some IO
}

JavaScript:

return WebAssembly.instantiate(bytes, imports).then((results) => {
    var tty_read = results.instance.exports.tty_read;
    ...
});

結論

(DeepLによる翻訳)
Zigを書くのは本当に楽しいです。いくつかの小さなプロジェクトを経て、今ではほとんどの構文を知っているし、必要なときにライブラリ関数がどのように見えるか、きちんと推測できるようになりました。長年のCプログラマーとして、Zigは私の考え方に合っています。Zigには明るい未来があると思います。

Discussion