0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zig loves COBOL: GnuCOBOLのファイル操作をZigで乗っ取る(EXTFHインターフェースの実装)

Posted at

はじめに

Zigに少し興味を持ったので検証してみました。何の知識もなかったので、ClaudeとCodexの力を頼りに開発を進めました。
テーマは、オープンソースのCOBOLコンパイラであるGnuCOBOLには、ファイルI/O処理を、外部のライブラリに置換えるEXTFHと呼ばれる機能があります。C等で作成された外部の3prtyのライブラリをREAD文、WRITE文で呼び出すことができる機能です。このライブラリを、C言語の代わりにZigを用いて作成してみました。

最古のコンピュータ言語であるCOBOLと、最新言語の一つであるZigの組み合わせです。また、この検証はとキュメントも含め、ほぼ100%、Vibe Codingしています。

1. GnuCOBOLの仕組み:Cへのトランスパイル

まず、オープンソースのCOBOLコンパイラであるGnuCOBOLがどのように動いているかを説明します。GnuCOBOLは、実はCOBOLを直接機械語にするのではなく、一度**C言語のソースコードにトランスパイル(変換)します。

一般的なビルドの流れは以下のようになります。

  1. トランスパイル: cobc コマンドがCOBOLで書かれたソースから、等価なC言語コードを生成します。
  2. コンパイル: システムのCコンパイラ(gcc/clang等)がCコードをコンパイルしオブジェクトファイルに変換します。(当然のことながら、生成されたオブジェクトはCのABIです)
  3. リンク: GnuCOBOLのランタイムライブラリ(libcob)とリンクして実行可能ファイルを生成します。
    リンク時に、コンパイルオプション"-fcallfh"にて、ユーザが作成したファイルハンドラーを含めることができます。

-fcallfh GnuCOBOLの拡張ファイルハンドラ(EXTFH)は、COBOLのREAD文/WITE文などのファイルI/O処理を、ユーザーがコーディングした外部関数への呼び出しに代替させることができます。これにより、COBOLのソースコードに大きな変更を加えることなく、様々な外部ファイルシステムに対応させることができます。
この「外部関数」をZigで書いてみようというのが、今回の試みです。
あまりピンとこないかもしれませんが、拡張ファイルハンドラの使いこなしは、COBOLのモダナイゼーションを実現する上での重要なテクニックの一つです。

2. GnuCOBOL拡張ファイルハンドラの仕組み

2.1.COBOLランタイムライブラリとのインターフェース

GnuCOBOLの拡張ファイルハンドラのインターフェースを簡単に説明します。
COBOLとZigをつなぐ共通言語となるのが、FCD3 (File Control Descriptor) と呼ばれるデータ構造です。これはファイルの状態や操作内容を保持するメモリブロックです。
COBOL側の定義(COPY句)と、Zig側の定義を見比べてみましょう。バイナリレベルでメモリレイアウトが完全に一致している必要があります。
GnuCOBOLのヘッダファイル(common.h)が手元にあれば、@cImport(組み込み関数)がコンパイル時にCの.h ファイルを解析して、Zigの構造体に変換してくれます。COBOLはかなり多様な形式のデータベースを扱うので、70項目ぐらいの定義があるのですが、Claude CodeやCodexが必要な定義を抽出してサブセットを作ってくれます。

COBOL (xfhfcd3.cpy): (実際は@cImportがlibcobのcommon.hを解析)

01 FH-FCD.
   05 FH-FCD-CALL-ID           USAGE BINARY-LONG.      *> 操作コード
   05 FH-FCD-HANDLE            USAGE BINARY-LONG.      *> ハンドル
   05 FH-FCD-STATUS            USAGE BINARY-SHORT.     *> 結果ステータス
   05 FH-FCD-FILENAME          PIC X(256).             *> ファイル名
   05 FH-FCD-RECORD-SIZE       USAGE BINARY-LONG.      *> レコード長
   05 FH-FCD-RECORD-POINTER    USAGE POINTER.          *> データへのポインタ
   *> ... (省略) ...

Zig (extern struct):

pub const FCD3 = extern struct {
    call_id: c_int,              // BINARY-LONG (32bit)
    handle: c_int,               // BINARY-LONG
    status: c_short,             // BINARY-SHORT (16bit)
    filename: [256]u8,           // PIC X(256)
    record_size: c_int,          // BINARY-LONG
    record_ptr: [*c]u8,          // POINTER
    // ... (省略) ...
};

Zigの extern struct はC ABI互換のメモリレイアウトを保証するため、COBOL(実質C)が書き込んだメモリをZig側で安全に読み書きできます。
Zigは文字型を持たず、文字は”ポインタ+長さ”で表現します。COBOLのPIC X(256)は[256]u8となります。cや他のモダンな言語よりも、COBOLに近い形で文字型を表現できます。
「データの範囲(長さ)を明確に意識して扱う」という規律がCOBOLとZigには共通してあります。

Zig loves COBOLを感じた瞬間でした。

2.2. データ駆動のディスパッチ:機能コードとメソッド

COBOLプログラムが READ FILE を実行したとき、実際には何が起きるのでしょうか?
EXTFHの仕組みでは、COBOLランタイムは「関数を直接呼ぶ」のではなく、「やりたいこと(機能コード)」をデータ(FCD3)に書き込んで、汎用ハンドラーに渡します。

これは、オブジェクト指向以前の「データとプロシージャ」の関係、あるいは原始的なメッセージパッシングに準じる方式です。

  • COBOL側: 「READしたい(call_id = 3)」「ファイルはこのハンドルだ」「結果はここにくれ」とFCD3構造体を埋めます。
  • Zig側: 受け取ったFCD3の call_id を見て、適切な処理(関数)へ振り分けます。
// Zigのエントリポイント
export fn czippfh(fcd_ptr: [*c]c_int) callconv(.C) void {
    const fcd: *FCD3 = @ptrCast(@alignCast(fcd_ptr));
    
    // 機能コードによるディスパッチ
    switch (fcd.call_id) {
        1 => handleOpen(fcd),       // OPEN
        2 => handleClose(fcd),      // CLOSE
        3 => handleRead(fcd),       // READ
        4 => handleWrite(fcd),      // WRITE
        // ...
        else => fcd.status = 9,     // 未知の操作エラー
    }
}

Zig側では、この「データとしての要求」を解釈し、実際のバックエンド処理へと繋ぎます。
実際の開発では、ここにさらにCで作られた、VBISAMをアクセスするためのドライバをリンクし、VBISAMと呼ばれる索引付きデータベースのAPIを呼ぶ処理を実装します。
今回は、この外部データベースとの結合度を弱め、外部データベースと、COBOLの実装を分離するために、抽象化層を作りました。

3. Zigによる抽象化:コンパイル時ポリモーフィズム

従来のC言語実装では、特定のISAMライブラリ(VBISAMなど)への依存がハードコードされがちでした。Goなどのモダンな言語では、インターフェースなどの機能で抽象化を行うことができます。Zigにも似たような機能がいくつか用意されています。今回は、Claude Codeの提案に従い、Zigの Tagged Union を使って抽象化層を作りました。

pub const IsamBackend = union(enum) {
    VBISAM: VbisamBackend,    // 従来のISAM (Cライブラリバインディング)
    SQLITE: SqliteBackend,    // SQLiteバックエンド
    MOCK: MockBackend,        // テスト用オンメモリバックエンド

    // ラッパー関数(switch文でディスパッチ)
    pub fn open(self: *IsamBackend, filename: []const u8, mode: OpenMode) !IsamFileHandle {
        return switch (self.*) {
            .VBISAM => |*b| b.open(filename, mode),
            .SQLITE => |*b| b.open(filename, mode),
            .MOCK   => |*b| b.open(filename, mode),
        };
    }
};

この設計により、実行時のオーバーヘッド(仮想関数テーブルの参照など)を排除しつつ、柔軟な切り替えが可能になります。VBISAMのライブラリに依存した実装も、Backendモジュールに閉じ込めることができるため、ドライバの切替えに関する柔軟性が生まれました。
追加backendの製造自体も試作レベルであれば、Claude Codeにより短時間での製造ができました。(ただし、外部データベースのソースコードや、ドキュメントが手元にあればの話ですが。)

4. Zigビルドシステム:バックエンドの自在な切り替えと生成

Zigの強力なビルドシステム (build.zig) を使うと、コンパイル時にどのbackendをリンクするかをオプション一で制御できます。これにより、単一のソースコードから、用途に応じた異なるバイナリを生成できます。また、Zigのビルドシステムは、Cのコンパイル機能も内包しているため、GnuCOBOLのコンパイル&リンクも統合できます。

build.zig の例:

const std = @import("std");

pub fn build(b: *std.Build) void {
    // コマンドラインオプションの定義: -Dbackend=xxx
    const backend_option = b.option(
        []const u8, 
        "backend", 
        "Choose ISAM backend: vbisam, sqlite, mock"
    ) orelse "vbisam"; // デフォルトは vbisam

    const exe = b.addExecutable(.{
        .name = "cobol_handler",
        .root_source_file = b.path("src/main.zig"),
        .target = b.standardTargetOptions(.{}),
        .optimize = b.standardOptimizeOption(.{}),
    });

    // オプションに応じた条件分岐
    const options = b.addOptions();
    options.addOption([]const u8, "backend_name", backend_option);
    exe.root_module.addOptions("build_options", options);

    // ライブラリのリンク切り替え
    if (std.mem.eql(u8, backend_option, "sqlite")) {
        exe.linkSystemLibrary("sqlite3");
    } else if (std.mem.eql(u8, backend_option, "vbisam")) {
        exe.linkSystemLibrary("vbisam");
    }

    b.installArtifact(exe);
}

この設定では、開発者は以下のようにコマンドを叩くだけで環境を切り替えられます。
VBISAM SQLiteと複数種類のbackendをコンパイル時に切替えられます。
Zigのビルドツールは、コンパイル時にどのbackendを使うかを選択し、それに関連するライブラリだけをlinkし、不要なコードは削除する機能を持ちます。

  • VBISAMビルド (VBISAM): zig build -Dbackend=vbisam
  • SQLiteビルド (SQLite): zig build -Dbackend=sqlite

5.Claude Code&Codexによるコーディング

 
 今回の試作は、100% Claude CodeとCodexで行いました。開発に着手したのは、2025年11月の終わりぐらいでした。Zigのコーディングは問題ありませんでしたが、一番多かったのはZigのバージョンの違いによるエラーでした。この部分でエラーになるコードは、Claude Codeが対応しましたが、エラーの影響がないが、古い形式のコードはソース残っていると思います。開発途上であり、破壊的な修正が入る事を厭わない、開発途上の言語については、このような課題があることは否めません。
 一方で、あまり精通していない言語でも、その言語のポテンシャルを引き出したコードが生成できるのは、エージェントコーディングならではの強さです。
 
 また、CodexやGeminiなど複数を使いましたが、GnuCOBOLのEXTFHのような仕組みを正しく理解したのは、Claude Codeだけでした。仕様書などを読ませて学習させれば、どのAgentでも問題なくコードが生成できるようになります。今後はSkillsなどで、この辺りのGnuCOBOL固有の仕様をAgentを跨いで共通に再利用できる事を検討したいと思います。Claude CodeからCodexに切替えたのは、単に、Codex5.2を使いたかったからという理由だけです。

6. Zigを使うメリットのまとめ

Zigを使うのは、今回が初めてでしたが、思いの外、GnuCOBOLとの相性は良好でした。やはり、CとのABIが共通であること、Cのライブラリとの連携をとりやすいことは大きな利点です。
また、C言語で同様のことを行うと、関数ポインタの構造体を手動で管理するか、プリプロセッサマクロ地獄になりがちです。Zigを採用することで以下の恩恵が得られました。

  • 安全性: Cの型とZigスライスへの変換境界で、境界チェックや型安全性を確保できる。ZigをCOBOLと外部Cライブラリの境界に入れることで、より安全性が高まる。
  • エラーハンドリング: COBOLの曖昧なステータスコード管理を、Zigの error union (!void) で堅牢にラップし、最終的な境界でのみCOBOL用コードに変換することができる。
  • ビルドシステム: GnuCOBOLとZigのライブラリにまたがるビルドフロー(Make/CMake等)をZig言語自体で記述・管理できる。また、コンパイル時に必要な機能だけを選択できる

など、多くの利点があることがわかりました。

おわりに

今回初めてZigを使いました。Claude Codeという強力なツールを介在させることで、短時間で試作品を製造できました。Zigには、想像していたよりもCOBLとの連携に適した多くの利点がありました。
今まで、多くの言語とGnuCOBOLの組み合わせを試してきましたが、まさに「Zig loves (Gnu)COBOL」な可能性を感じました。リリースされてから10年。まだver1.0にも到達していない言語であり、今しばらく成長を待つ必要があります。しばらくは温かく見守りながら、少しずつ学び、遊んでみようと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?