hkoba blog

プログラマーです。プログラミング言語ミーハーです。ツッコミ歓迎です。よろしくどうぞ(能代口調)

Cのマクロの Rust 化 - libperl-rs 現状報告

要約:(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ヶ月を要しました。

macrogen の CC usage

libperl-sys との統合の CC usage

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

ここまで読んでくださり、ありがとうございました!