要約:(XS も含めて)Rust と Perl の相性が良くなったよ!

例えば perlxstut の例2, is_even() が、 libperl-rs でこう書けるようになりました:
use libperl_rs::{xs_boot, xs_sub, IV}; /// `Mytest::is_even($n)` — returns true if `$n` is even. /// (perlxstut EXAMPLE 2.) #[xs_sub] fn is_even(n: IV) -> bool { n % 2 == 0 } xs_boot! { package = "Mytest"; subs = [is_even]; }
どんな問題が有って、どうやって改善したか
Perl の API を Rust から叩くためのライブラリ、 libperl-rs というものを作っています。 これは Rust の bindgen (C のヘッダーから Rust の宣言を自動的に生成するもの)を利用して作ってきました。 これだけでもある程度は役に立つのですが、大きな問題が残っていました。それが C のマクロ関数です。
Perl インタープリターの内部構造は、Perl のバージョンの進化に伴って変化してきました。 そのバージョンごとの差異を吸収するために、Perl の API は大量の C マクロ関数を使って定義されています。 また、スレッド対応の有無の抽象化も C マクロ関数が担っています。
ところが Rust の bindgen は Cのマクロ関数を一切無視します。(static inline 関数も無視します)。 このため、Perl の API で定義された抽象化の仕組みが、libperl-rs 側では一切利用できない状況でした。
例えば昨年末の記事(Rust の libperl-sys で XS を書いてみた - hkoba blog)の時点の libperl-rs では、最初に挙げた is_even() ですら
この長さでした。
fn is_even(my_perl: &mut PerlInterpreter, cv: *mut CV) -> () { // dSP let sp = my_perl.Istack_sp; // dAXMARK let mut ax: Stack_off_t = unsafe {*my_perl.Imarkstack_ptr}; my_perl.Imarkstack_ptr = unsafe {my_perl.Imarkstack_ptr.sub(1)}; // POPMARK let mark = unsafe {my_perl.Istack_base.add(ax as usize)}; ax = ax+1; // dITEMS let items = unsafe {sp.offset_from(mark)}; if items != 1 { let msg = "input"; unsafe {Perl_croak_xs_usage(cv, msg.as_bytes().as_ptr() as *const i8)}; } // int input = (int)SvIV(ST(0)) let src = unsafe {*my_perl.Istack_base.add((ax + 0) as usize)}; let input = SvIV(my_perl, src); let RETVAL = (input % 2) == 0; println!("input {} RETVAL {}", input, RETVAL); let targ = unsafe {Perl_sv_newmortal(my_perl)}; // PUSHi((IV)RETVAL); unsafe { Perl_sv_setiv(my_perl, targ, RETVAL as i64) }; unsafe {*sp = targ}; let off = 1; my_perl.Istack_sp = unsafe {my_perl.Istack_base.add((ax + (off - 1)).try_into().unwrap())}; return () } #[unsafe(no_mangle)] pub extern "C" fn boot_Mytest(my_perl_ptr: *mut PerlInterpreter, _cv: *mut CV) -> () { if my_perl_ptr.is_null() { panic!() } static NAME1: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"Mytest::is_even\0") }; unsafe { Perl_newXS_deffile(my_perl_ptr, NAME1.as_ptr(), Some(is_even_C)) }; unsafe { Perl_xs_boot_epilog(my_perl_ptr, 1); }; }
この問題を解決するために Claude Code に作成させたのが libperl-macrogen です。 これは Perl の API で定義された C ヘッダーに特化した、C のマクロ(と inline)関数から Rust の関数を生成するトランスパイラーです。
Claude Code (を始めとした、コーディングエージェント)がプログラミング言語の処理に長けていることは、昨年に Claude Code を触っていた言語好きのプログラマーにはよく知られていることです。そこで私は既存のCコンパイラー OSS である TinyCC のプリプロセッサーと意味解析部分を参考にして、Perl の API に特化した専用のトランスパイラーを作成するように Claude Code に指示することを思いつきました。コーディングエージェントは体力おばけなので、人力では根気の続かないタスクも任せられると考えました。
オリジナルのライセンスを尊重するため、このトランスパイラー部分だけは別プロジェクトとしてプロジェクトを開始しました。
掛かった時間
昨年の冬休みにプロジェクトを開始して、すぐに出来上がるかと思いましたが…結局リリースまで4ヶ月を要しました。


libperl-rs をどう育てていくかはまだ迷い中です。不足している機能もたくさん有るはず…ご意見いただければ嬉しいです。
ここまで読んでくださり、ありがとうございました!