評価15: C言語連携 - 評価基準

評価の目的

この評価では、ZigとC言語の相互運用能力を確認します。Cヘッダーのインポート、POSIX APIの活用、C互換関数のエクスポート、Cライブラリのラッピングなど、実践的なC連携スキルを評価します。

学習目標

この評価を通じて、以下の能力を確認します:

  • @cImport を使ったCヘッダーのインポート
  • C言語の関数とデータ型の適切な使用
  • export キーワードによるC互換関数の作成
  • Cライブラリの型安全なラッピング
  • メモリ管理とリソース解放の適切な処理

評価項目

1. Cヘッダーのインポート(20点)

項目 配点 評価基準
ヘッダーのインポート 5点 @cImport と @cInclude の正しい使用
C関数の呼び出し 5点 printf、sqrt、strlenなどが正しく動作
メモリ管理 5点 malloc/freeの適切な使用とリーク防止
文字列の扱い 5点 null終端文字列の正しい処理

確認ポイント:

  • [ ] @cImport ブロックが正しく記述されている
  • [ ] C関数が正常に呼び出される
  • [ ] メモリリークがない
  • [ ] null終端文字列を適切に作成・変換

良い実装例:

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

pub fn printMessage(message: []const u8) void {
    const c_str = std.heap.page_allocator.dupeZ(u8, message) catch return;
    defer std.heap.page_allocator.free(c_str);

    _ = c.printf("%s\n", c_str.ptr);
}

pub fn calculateSqrt(x: f64) f64 {
    return c.sqrt(x);
}

pub fn duplicateData(data: []const u8) ?[*]u8 {
    const ptr = c.malloc(data.len + 1);
    if (ptr == null) return null;

    const result: [*]u8 = @ptrCast(ptr);
    @memcpy(result[0..data.len], data);
    result[data.len] = 0;  // null終端

    return result;
}

// 悪い例:メモリリーク
pub fn duplicateData(data: []const u8) ?[*]u8 {
    const ptr = c.malloc(data.len);  // +1 がない
    // コピー処理なし
    return @ptrCast(ptr);  // リークの原因
}

2. POSIX APIの活用(20点)

項目 配点 評価基準
ファイル作成・書き込み 6点 open、write、closeが正しく動作
ファイル読み込み 6点 read操作が適切に実装されている
ファイル情報取得 4点 statでファイルサイズを取得できる
エラーハンドリング 4点 POSIXエラーを適切に処理

確認ポイント:

  • [ ] ファイルディスクリプタが正しく管理されている
  • [ ] エラーコードを適切に処理
  • [ ] リソースリークがない
  • [ ] パーミッションが適切に設定されている

POSIX API使用例:

const c = @cImport({
    @cInclude("fcntl.h");
    @cInclude("unistd.h");
    @cInclude("sys/stat.h");
});

pub fn writeFile(path: []const u8, data: []const u8) !void {
    const path_z = try std.heap.page_allocator.dupeZ(u8, path);
    defer std.heap.page_allocator.free(path_z);

    const fd = c.open(
        path_z.ptr,
        c.O_WRONLY | c.O_CREAT | c.O_TRUNC,
        0o644
    );
    if (fd < 0) return error.OpenFailed;
    defer _ = c.close(fd);

    const written = c.write(fd, data.ptr, data.len);
    if (written < 0) return error.WriteFailed;
}

pub fn getFileSize(path: []const u8) !usize {
    const path_z = try std.heap.page_allocator.dupeZ(u8, path);
    defer std.heap.page_allocator.free(path_z);

    var st: c.struct_stat = undefined;
    if (c.stat(path_z.ptr, &st) < 0) {
        return error.GetSizeFailed;
    }

    return @intCast(st.st_size);
}

3. C互換関数のエクスポート(20点)

項目 配点 評価基準
export関数の実装 6点 gcd、lcm、factorialが正しく実装
C互換型の使用 6点 c_int、c_longlongを適切に使用
ヘッダーファイル 4点 対応するCヘッダーが正しく記述されている
アルゴリズムの正確性 4点 数学的に正しい計算結果

確認ポイント:

  • [ ] export キーワードが使用されている
  • [ ] C互換の型を使用している
  • [ ] 関数シグネチャがヘッダーと一致
  • [ ] Cから呼び出し可能

エクスポート関数の例:

// ユークリッドの互除法
export fn gcd(a: c_int, b: c_int) c_int {
    var x = a;
    var y = b;
    while (y != 0) {
        const temp = y;
        y = @rem(x, y);
        x = temp;
    }
    return if (x < 0) -x else x;
}

export fn lcm(a: c_int, b: c_int) c_int {
    if (a == 0 or b == 0) return 0;
    return @divTrunc((a * b), gcd(a, b));
}

export fn factorial(n: c_int) c_longlong {
    if (n <= 0) return 1;
    var result: c_longlong = 1;
    var i: c_int = 2;
    while (i <= n) : (i += 1) {
        result *= i;
    }
    return result;
}

export fn sumArray(arr: [*]const c_int, len: c_int) c_int {
    var sum: c_int = 0;
    var i: c_int = 0;
    while (i < len) : (i += 1) {
        sum += arr[@intCast(i)];
    }
    return sum;
}

対応するヘッダーファイル:

#ifndef MATHLIB_H
#define MATHLIB_H

#ifdef __cplusplus
extern "C" {
#endif

int gcd(int a, int b);
int lcm(int a, int b);
long long factorial(int n);
long long power(int base, int exp);
int sumArray(const int* arr, int len);

#ifdef __cplusplus
}
#endif

#endif // MATHLIB_H

4. Cライブラリのラッピング(20点)

項目 配点 評価基準
DateTime構造体 6点 時刻情報を適切にラップ
現在時刻取得 5点 time、localtimeを正しく使用
時刻変換 5点 Unix時刻との相互変換が正しい
フォーマット 4点 人間が読める形式に変換

確認ポイント:

  • [ ] C APIをZigの型システムでラップ
  • [ ] エラーハンドリングが適切
  • [ ] メモリ安全性が保たれている
  • [ ] 使いやすいインターフェース

ラッパーの例:

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

pub const DateTime = struct {
    year: u32,
    month: u32,
    day: u32,
    hour: u32,
    minute: u32,
    second: u32,

    pub fn now() !DateTime {
        const t = c.time(null);
        if (t == -1) return error.GetTimeFailed;

        const tm_ptr = c.localtime(&t);
        if (tm_ptr == null) return error.LocalTimeFailed;

        const tm = tm_ptr.*;
        return DateTime{
            .year = @intCast(tm.tm_year + 1900),
            .month = @intCast(tm.tm_mon + 1),
            .day = @intCast(tm.tm_mday),
            .hour = @intCast(tm.tm_hour),
            .minute = @intCast(tm.tm_min),
            .second = @intCast(tm.tm_sec),
        };
    }

    pub fn format(
        self: DateTime,
        allocator: std.mem.Allocator,
    ) ![]u8 {
        return try std.fmt.allocPrint(
            allocator,
            "{d:0>4}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}",
            .{
                self.year,
                self.month,
                self.day,
                self.hour,
                self.minute,
                self.second,
            }
        );
    }
};

5. ボーナス課題(20点)

項目 配点 評価基準
SQLiteラッパー 10点 Database、Statementクラスが動作
HTTPクライアント 10点 libcurlを使ったGET/POSTリクエスト

ボーナス評価ポイント:

  • [ ] SQLiteの準備、実行、結果取得が正しい
  • [ ] HTTPリクエストとレスポンスの処理が適切
  • [ ] エラーハンドリングが完璧
  • [ ] 実用的なAPI設計

チェックリスト

実装前の確認

  • [ ] 必要なCヘッダーを特定している
  • [ ] C APIのドキュメントを読んでいる
  • [ ] メモリ管理の責任を理解している
  • [ ] エラーコードの意味を把握している

実装後の確認

  • [ ] すべてのプログラムが正しくコンパイル
  • [ ] C関数の呼び出しが成功
  • [ ] メモリリークがない
  • [ ] エラーケースが適切に処理されている

コード品質の確認

  • [ ] null終端文字列の処理が正確
  • [ ] リソースが確実に解放されている
  • [ ] 型変換が安全に行われている
  • [ ] Cコードとの互換性が保たれている
  • 合格基準

    レベル 点数 説明
    優秀 90-100 すべての機能を完璧に実装、ボーナス課題も完了
    良好 80-89 マンダトリー課題を完全実装、ボーナス課題の一部完了
    合格 64-79 マンダトリー課題の80%以上を正しく実装
    要再提出 0-63 C連携が動作しないまたはメモリリークあり

    最低合格ライン: 64点以上(マンダトリー80点満点の80%)

    よくある減点ポイント

    1. null終端文字列の処理ミス(-5〜10点)

    // NG: null終端なし
    pub fn duplicateData(data: []const u8) ?[*]u8 {
        const ptr = c.malloc(data.len);
        @memcpy(@as([*]u8, @ptrCast(ptr))[0..data.len], data);
        return @ptrCast(ptr);  // null終端がない
    }
    
    // OK: null終端あり
    pub fn duplicateData(data: []const u8) ?[*]u8 {
        const ptr = c.malloc(data.len + 1);  // +1 が重要
        const result: [*]u8 = @ptrCast(ptr);
        @memcpy(result[0..data.len], data);
        result[data.len] = 0;  // null終端
        return result;
    }
    

    2. リソースリーク(-10〜20点)

    // NG: ファイルディスクリプタのリーク
    pub fn writeFile(path: []const u8, data: []const u8) !void {
        const fd = c.open(path, c.O_WRONLY | c.O_CREAT);
        _ = c.write(fd, data.ptr, data.len);
        // close を忘れている!
    }
    
    // OK: 確実にクローズ
    pub fn writeFile(path: []const u8, data: []const u8) !void {
        const fd = c.open(path, c.O_WRONLY | c.O_CREAT, 0o644);
        if (fd < 0) return error.OpenFailed;
        defer _ = c.close(fd);  // 確実にクローズ
    
        const written = c.write(fd, data.ptr, data.len);
        if (written < 0) return error.WriteFailed;
    }
    

    3. 型変換の誤り(-5点)

    // NG: 危険な型変換
    const result: [*]u8 = ptr;  // 型チェックなし
    
    // OK: 明示的な型変換
    const result: [*]u8 = @ptrCast(@alignCast(ptr));
    

    学習の手引き

    C連携の基礎

  • @cImport の理解
- Cヘッダーのインポート方法 - 名前空間の扱い - 型の対応関係

  • メモリ管理
- malloc/free の使用 - null終端文字列の作成 - リソースの確実な解放

  • エラーハンドリング
- POSIXエラーコードの処理 - Zigのエラーセットへの変換 - エラーメッセージの取得

効果的な学習方法

  • 小さいプログラムから
- 単純なC関数の呼び出し - 徐々に複雑なAPIへ

  • Cドキュメントの活用
- man ページの読み方 - APIリファレンスの参照

  • デバッグツールの使用
- valgrind でメモリリーク検出 - gdb でデバッグ

ピアレビューのポイント

評価者は以下の点を確認してください:

1. ビルドと実行

# Part 1
zig build-exe part1_import.zig -lc
./part1_import

# Part 2
zig build-exe part2_posix.zig -lc
./part2_posix

# Part 3
zig build-exe part3_export.zig -lc
./part3_export

# Part 4
zig build-exe part4_wrapper.zig -lc
./part4_wrapper

2. コード確認

  • C連携の正確性
  • メモリリークの有無
  • エラーハンドリングの適切性
  • ヘッダーファイルとの整合性

3. 理解度の確認

質問例:
  • 「null終端文字列はなぜ必要ですか?」
  • 「malloc と Zigのアロケータの違いは?」
  • 「POSIXエラーはどう処理しますか?」
  • フィードバックの例

    良いフィードバック

    > 「C言語連携の実装が非常に優れています。@cImport の使用が正しく、C関数の呼び出しもエラーなく動作しています。null終端文字列の処理も適切で、メモリリークもありません。POSIX APIの実装では、ファイルディスクリプタの管理が完璧で、deferを使った確実なリソース解放が実現できています。export関数のヘッダーファイルも整合性が取れており、実用的です。」

    改善が必要な場合のフィードバック

    > 「基本的なC連携は動作していますが、duplicateData関数でnull終端文字列の処理が抜けています。data.len + 1 でメモリを確保し、最後にnull文字を追加してください。また、POSIX APIのwriteFile関数でcloseを忘れており、ファイルディスクリプタがリークしています。deferを使って確実にクローズするように修正してください。」

    参考資料

  • Zig C Interop: https://ziglang.org/documentation/master/#C
  • POSIX: https://pubs.opengroup.org/onlinepubs/9699919799/
  • SQLite C API: https://www.sqlite.org/capi3ref.html
  • libcurl: https://curl.se/libcurl/c/

まとめ

ZigとC言語の相互運用は、既存のエコシステムを活用する上で重要です。この評価を通じて、Cヘッダーのインポート、POSIXAPIの使用、C互換関数のエクスポート、Cライブラリのラッピングなど、実践的なC連携スキルを習得しました。これらの技術により、Zigで既存のCライブラリを活用し、またZigのコードをC言語から呼び出すことが可能になります。