🪚

ruby gemをzigで書こう

2022/12/14に公開3

こんにちは、kaztoです。

本記事はZigアドベントカレンダー14日目の記事です。

Ziglang、とても良い言語ですよね。

今のところまだ認知度が高くないので、アドベントカレンダーを立ててみたんですが、案の定と言いますか参加率はまだまだこれから。

zigの紹介

2022年12月現在、バージョンは0.10.0、まだまだ破壊的な仕様変更が加わっている状態です。

なぜこんな作り途中の言語を推すのか。

公式サイトには、以下のように特徴を挙げています。

  • 隠された制御フローはありません。
  • 隠されたメモリ割り当てはありません。
  • プリプロセッサ、マクロもありません。

プリプロセッサやマクロが無いことが長所になるのか、については、Zigに関しては長所と言えましょう。

Zigの魅力は、シンプルな言語仕様にあると考えています。

ビルドシステム

また、Zigには言語自体にビルドシステムが備わっています。C言語に対するMakeコマンド、Makefileの立ち位置のものです。

これが強力で、Makefileの代替として十分役に立つと考えています。例については後述したいと思います。

さらには、Zigのビルドシステムは、C/C++のソースもビルドできます。ZigコンパイラがCコンパイラのラッパとして動作します。

C/C++資源の活用

上記で、ZigのソースとC/C++のソースをいっしょくたにビルドできることを示しましたが、ビルドしたものが相互に呼び出されないと意味がありません。もちろん、ZigでビルドされたバイナリはC/C++の呼び出し規則に準拠しており、FFIなしに相互に呼び出すことができます。

長い前振り

ここまで読んでくださり、ありがとうございます。ええ加減Rubyどこに関係しとんねんとお思いのみなさま。ピースは出揃っております。

  • 使いやすいビルドシステム
  • C/C++との親和性

ここで問題です。Rubyの開発言語は何でしょうか?

Rubyそのものだったり、近年YJITで採用されたRustは置いておいてください。

そう、C言語ですね。

Ruby gemの大半はRubyそのもので書かれていますがネイティブエクステンションは大半がC/C++で記述されています。

とはいえ、令和にもなってC言語を覚えたくはないですよね?メモリリーク怖いですよね?マクロで魔改造されたコードを追いかけるのは大変ですよね?

そこでZigですよ

Zigでプログラミングすることは、C言語でプログラミングすることよりも生産性が高いはず、と見込んでおります(個人の感想レベルですが)。

でも、bundle gemコマンドとか対応してないじゃん

Rubyでネイティブエクステンションをビルドする手順をおさらいしましょう。

  • bundle gemでひな形を生成する
  • extconf.rbを記述してMakefileを生成する
  • Makefileが各コンパイラを呼び出してビルドする
  • できたバイナリをパッケージングする

上記のうち、extconf.rbでMakefileを生成するところで、MakefileからZigコンパイラを呼ぶことに対応すれば、Zigで記述されたソースもビルドできそうです。

「あるよ…」(って言いたかった)

extconf.rbでZigを呼び出すためのMakefileを作成するパッケージを作成中です。本当はアドベントカレンダーに合わせてリリースしたかったのですが、間に合いませんでした。。。

extconf.rbは、bundle gemで生成した直後は以下のコードになります。

# frozen_string_literal: true

require "mkmf"

create_makefile("example/example")

これを、mkmf-zigパッケージを用いて以下のように変更します。

# frozen_string_literal: true

require "mkmf"
require "mkmf/zig"

create_zig_makefile("example/example")

そうすると、Zigを呼び出すようなMakefileが作成される、というイメージです。

つぎに、extconf.rbがあるディレクトリでzig init-libを実行します。build.zigとsrc/main.zigが生成されます。

example.zigにおいてruby.hをインクルードし、rubygemsのお作法にしたがって呼び出せる状態にします。

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

var rb_mExample: ruby.VALUE = 0;

(省略)

この状態でzig buildを実行すると、./zig-out/example/libexample.soがビルドされます。

この.soファイルをextconf.rbと同じディレクトリにexample.soと移動してやり、gem buildすれば.gemファイルが作成されるようにしたいと考えています。この辺はMakefileがよしなにやってくれるようにする予定です。

まとめ

以上のように、ZigとRubyはとっても仲良しになれる可能性を秘めています。Rubyistの皆さん、Zigを使いましょう!!!(なんのアドベントカレンダーだかわかんなくなっちゃった!)

Discussion

fjcfjc

すごいですね! ありがとうございま。

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

fn rb_return_nil() callconv(.C) ruby.VALUE {
    return ruby.Qnil;
}

export fn Init_librubyzig() void {
    const mod = ruby.rb_define_module("RubyZig");
    ruby.rb_define_method(mod, "return_nil", rb_return_nil, 0);
}

zig build-lib ...

rubyzig.zig:9:46: error: expected type '*const fn(...) callconv(.C) c_ulong',
                                  found '*const fn() callconv(.C) c_ulong'

edit...

-export fn rb_return_nil() ruby.VALUE {
+export fn rb_return_nil(...) ruby.VALUE {

zig build-lib ...

rubyzig.zig:3:8: error: non-extern function is variadic

Ziglang issue 515: support exporting a c-style var args function (Open)
Ziglang pull request 13914: implement defining C variadic functions (Open)

https://github.com/ziglang/zig/issues/515
https://github.com/ziglang/zig/pull/13914

fjcfjc

Need to replace anytype with the correct types, but this works:

const ruby = @cImport({ @cInclude("ruby.h"); });
fn rb_return_nil() callconv(.C) ruby.VALUE { return ruby.Qnil; }

fn rb_define_method(klass: anytype, name: anytype, function: anytype, argc: anytype) void {
    ruby.rb_define_method(klass, name, @ptrCast(*const fn(...) callconv(.C) ruby.VALUE, &function), argc);
}

export fn Init_librubyzig() void {
    const RubyZig = ruby.rb_define_class("RubyZig", ruby.rb_cObject);
    rb_define_method(RubyZig, "return_nil", rb_return_nil, 0);
}
$ ./zig build-lib ...
$ ruby -I. -rlibrubyzig -e 'p RubyZig, RubyZig.new.return_nil'
RubyZig
nil