TL; DR
- 定数文字列は通常の文字列と識別するために
"\xff"が付けられる - クラスは構造体になる
- 親クラスのインスタンスメソッド呼び出しは構造体のキャストで実現
- 戻り値型の混在は(今のところ)未対応でセグフォになる
はじめに
Rubyソースコードからシングルバイナリを生成するコンパイラ「Spinel」が発表されました。
Spinelでは、RubyをCのソースコード経由でコンパイルしているため、gccの対応アーキテクチャや最適化の恩恵を受けられます。LLMで開発されたということもあって話題になっています。
一方で、eval や require が使えない等機能の割り切りもあり、「Rubyをシングルバイナリ化する処理系」というよりは「Rubyでシングルバイナリのツールが作れる処理系」と考えるのが良さそうです。
こちらの記事で詳しい挙動について紹介されていました。
本記事では、Spinelが生成するソースコードを(軽く)眺めていきます。
注意
本記事の挙動は執筆時点(2026/05)での内容です。
まずはサンプルコード
リポジトリにも載っているフィボナッチ数列のサンプルコードです。
def fib(n)
if n < 2
n
else
fib(n - 1) + fib(n - 2)
end
end
puts fib(34)
以下のようにコンパイルされます。
/* Generated by Spinel AOT compiler */
#include "sp_runtime.h"
static const char *sp_sym_to_s(sp_sym id){(void)id;return "";}
static mrb_int sp_fib(mrb_int lv_n);
static mrb_int sp_fib(mrb_int lv_n) {
if ((lv_n < 2)) {
return lv_n;
} else {
return (sp_fib((lv_n - 1)) + sp_fib((lv_n - 2)));
}
return 0;
}
typedef struct{const char**data;mrb_int len;}sp_Argv;
static sp_Argv sp_argv;
int main(int argc,char**argv){
sp_argv.len=argc-1;sp_argv.data=(const char**)malloc(sizeof(const char*)*(argc>1?argc-1:1));{int _i;for(_i=0;_i<sp_argv.len;_i++)sp_argv.data[_i]=sp_str_dup_external(argv[_i+1]);}
printf("%lld\n", (long long)sp_fib(34));
return 0;
}
トップレベルの処理はすべてmain関数内に記載されます。
専用の型 mrb_int は int64_t のエイリアスです。
文字列
続いて文字列です。
s = "abc"
コンパイルすると、&("\xff" "abc")[1] という char のポインタ型になります。
// ...
const char *lv_s = (&("\xff")[1]);
SP_GC_ROOT(lv_s);
lv_s = (&("\xff" "abc")[1]);
// ...
この先頭についている \xff は、GCが定数リテラルをそれ以外の文字列と区別するために付けられたマーカーです。(定数の場合はGC不要)
# Prepend 0xff marker byte so GC can identify static literals.
# Return form: (&("\xff" "content")[1]) — same pointer value as the
# legacy ("\xff" "content" + 1) idiom, but uses array indexing so
# clang doesn't flag it under -Wstring-plus-int.
"(&(\"\\xff\" " + result + "\")[1])"
GCへの登録は SP_GC_ROOT の部分で行われていました。
#define SP_GC_ROOT(v) int __attribute__((cleanup(_sp_gc_root_pop))) _SP_GC_CONCAT(_sp_gcr_, __COUNTER__) = _sp_gc_root_push((void**)&(v))
__attribute__ はGCCの拡張機能で、変数に属性を付けられます。ここでは cleanup を指定することで、変素がスコープを抜けたタイミングで解放されます。
クラスと継承
クラスは、インスタンス変数に対応する構造体とメソッドに対応する関数にコンパイルされました。
class A
def initialize(a)
@a = a
end
def a()
"a"
end
end
typedef struct sp_A_s sp_A;
struct sp_A_s {
mrb_int iv_a;
};
static sp_A *sp_A_new(mrb_int lv_a);
static inline void sp_A_initialize(sp_A *self, mrb_int lv_a);
static inline const char * sp_A_a(sp_A *self);
static inline sp_A *sp_A_new(mrb_int lv_a) {
SP_GC_SAVE();
sp_A *self = (sp_A *)sp_gc_alloc(sizeof(sp_A), NULL, NULL);
SP_GC_ROOT(self);
self->iv_a = lv_a;
SP_GC_RESTORE();
return self;
}
static inline void sp_A_initialize(sp_A *self, mrb_int lv_a) {
self->iv_a = lv_a;
}
static inline const char * sp_A_a(sp_A *self) {
SP_GC_SAVE();
SP_GC_ROOT(self);
return (&("\xff" "a")[1]);
return (&("\xff")[1]);
}
メソッド名が sp_{クラス名}_{メソッド名} になることで別クラスとの衝突を防いでいます。
また、継承した場合、子クラスのインスタンスが親クラスのインスタンスメソッドを呼び出す際にはキャストをしています。
class B < A
def b()
"b"
end
end
b = B.new(1)
b.a
// ...
lv_b = sp_B_new(1);
sp_A_a((sp_A *)lv_b);
// ...
リスコフの置換原則が守られている限り、このキャストでクラッシュすることはないはずです。
メソッドの型
メソッドの型は変数から推論されますが、戻り値が混在している場合は正しく扱えませんでした。コンパイルエラーにはならずSegmentation Faultが起きるので、実装時に注意する必要があります。
def fizzbuzz(n)
if n % 15 == 0
"fizzbuzz"
elsif n % 3 == 0
"fizz"
elsif n % 5 == 0
"buzz"
else
n
end
end
static inline const char * sp_fizzbuzz(mrb_int lv_n) {
if ((sp_imod(lv_n, 15) == 0)) {
return (&("\xff" "fizzbuzz")[1]);
} else
if ((sp_imod(lv_n, 3) == 0)) {
return (&("\xff" "fizz")[1]);
} else
if ((sp_imod(lv_n, 5) == 0)) {
return (&("\xff" "buzz")[1]);
} else {
// mrb_int型を返しているので型が合わない!
return lv_n;
}
return (&("\xff")[1]);
}
おわりに
以上、ざっくりとSpinelの生成物を眺めてみた紹介でした。LLMの力で改修が着々と進められているようなので、安定したら実際にCLIを作ってみたいと思います。