hkoba blog

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

libperl-rs の話

f:id:hkoba501:20200329224058p:plain
Rustだからカニを貼ってみた

昨年2019の夏頃から libperl-rs というライブラリを作っています(現状では仕事とは無関係の、純然たる自宅研究です。進みも間欠的です)。これは Perl5 のランタイムライブラリーである libperl を Rust から呼び出すためのラッパーライブラリーです。

github.com

libperl-rs については、これまでは YAPC Japan 2019 名古屋で発表した以外は、 これといった情報開示をしてきませんでした。

hkoba.github.io

別に秘密というわけではなく、 単に blog を書くことを面倒臭がっていただけで、 github 上には 少しずつ進展を出してはいました。ですが最近、私が前もって現状を整理して公開していれば、誰かさんを困らせなくて済んだのかもなー…と思うこともあり…

ちょっと現状を整理して開示しておこうと思います。

libperl-rs の開発目標、libperl-rs で何を出来るようにしたいか

私が libperl-rs で一番やりたいことは、 Perl プログラムの AST (OP Tree) の解析です。 特に、動的に生成されるスクリプトの AST を解析し、その実行前にエラーを検出したり、 Language Server やデバッガーを提供したり…ということが出来るような道を開きたいと考えています。 (そこまでたどり着けるとは言い切れませんが…)

libperl-rs の現状

現在の libperl-rs は3つの crate から構成されています。

  1. libperl-config - env perl からビルド情報を取り出すためのライブラリーです。 2., 3. のビルドに用います。
  2. libperl-sys - env perl 付属の libperl に対する Rust binding を生成する crate です。bindgen を用いて C のヘッダーから Rust のコードを自動生成します。
  3. libperl-rs - libperl-sys の機能を Rust らしく扱えるようにするためのラッパーライブラリーです。
%  tree -L 2
.
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── build.rs
├── examples
│   ├── 000_perl_parse.rs
│   ├── 001_perl_parse_args.rs
│   ├── 100_scan_ops.rs
│   ├── 101_scan_ops_debug.rs
│   ├── 102_padname_type.rs
│   ├── 103_scan_op_tree.rs
│   ├── 104_enum_op_tree.rs
│   ├── 105_scan_stash.rs
│   ├── 106_scan_allpackage.rs
│   ├── 107_scan_subs_in_a_file.rs
│   ├── 108_scan_subs_in_a_file2.rs
│   ├── 109_scan_subs_intro1.rs
│   ├── 110_call_method.rs
│   └── eg
├── libperl-config
│   ├── Cargo.lock
│   ├── Cargo.toml
│   └── src
├── libperl-sys
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── LICENSE
│   ├── build.rs
│   ├── examples
│   ├── script
│   ├── src
│   └── wrapper.h
├── release.toml
├── runtest-docker.zsh
├── src
│   ├── lib.rs
│   └── perl.rs
└── target
    ├── debug
    └── rls

現状では 1., 2. については API はほぼ安定しているのではないかと思います。(改良の余地は有るかもしれません)

問題は 3. の libperl-rs です。 2. のbindgen によって生成された libperl-sys は Perl の C API からマクロを取り去った、裸の API です。しかも threaded 版と非スレッド版の2系統が存在します。それを Rust の層で抽象化し直す必要があります。 私がまだ Rust の修行中であることもあり、一発で正解にたどり着く自信はありません。 とても API が固まったとは言えない状況です。

そのため、現在は主として examples ディレクトリーの下に 実験的なコードを書き、その中で今後 libperl-rs の本体に組み入れると良さそうなコードを eg サブディレクトリーに作り貯めている所です。 皆さんのツッコミをお待ちしております。

今の libperl-rs で出来ること

今の段階の libperl-rs で出来ることは、どれも B モジュール(とても頑張れば)出来ることばかりだと思います。

とはいえ、OP Tree に対してパターンマッチが出来るようになりつつあることは、 少なからぬメリットなのではないでしょうか。以下は Perl のサブルーチンの冒頭から my (...) = @_ に相当する AST を抜き出すための、パターンマッチのコードです。 (少し冗長ですが)

        let ast = op_extractor.extract(cv, CvROOT(cv));
        
        match ast {
            Op::UNOP(opcode::OP_LEAVESUB, _
                     , Op::LISTOP(opcode::OP_LINESEQ, _
                                  , Op::COP(opcode::OP_NEXTSTATE, body), _), _) => {
                println!("preamble!");
                match body {
                    Op::BINOP(opcode::OP_AASSIGN, _
                              , Op::UNOP(opcode::OP_NULL, _
                                         , Op::OP(opcode::OP_PADRANGE, _, _
                                                  , Op::UNOP(opcode::OP_RV2AV, _
                                                             , Op::PADOP(opcode::OP_GV, _
                                                                         , Sv::GLOB { name: ref nm, .. })
                                                             , _))
                                         , lvalue)
                              , _) if nm == "_" => {
                        println!("first array assignment from @_, lvalue = {:?}"
                                 , match_param_list(lvalue));
                        
                    }
                    _ => {
                        println!("first statement is not an array assignment");
                    }
                }
            }
            _ => {
                println!("doesn't match")
            }
        }

他にも (perl_parse の作る) 静的な OP Tree を調べるコードだけではなく、 perl_parse 後に任意のクラスの任意のメソッドを Rust から呼び出してみるサンプルも、(まだまだベタな書き方ですし不足もありますが)動き始めています(抜粋)。

#[cfg(all(perl_useithreads,perlapi_ver26))]
fn call_list_method(perl: &mut Perl, class_name: String, method_name: String, args: Vec<String>) -> Result<Vec<Sv>,String>
{

    let mut my_perl = perl.my_perl();

    // dSP
    let mut sp = my_perl.Istack_sp;

    // ENTER
    unsafe_perl_api!{Perl_push_scope(perl.my_perl)};

    // SAVETMPS
    unsafe_perl_api!{Perl_savetmps(perl.my_perl)};

    // PUSHMARK(SP)
    perl.pushmark(sp);
    
    // (... argument pushing ...)
    // EXTEND(SP, 1+method_args.len())
    sp = unsafe_perl_api!{Perl_stack_grow(perl.my_perl, sp, sp, (1 + args.len()).try_into().unwrap())};
    
    for s in [&[class_name], args.as_slice()].concat() {
        sp_push!(sp, perl.str2svpv_mortal(s.as_str()));
    }

    // PUTBACK
    my_perl.Istack_sp = sp;

    // call_method
    let cnt = unsafe_perl_api!{Perl_call_method(perl.my_perl, method_name.as_ptr() as *const i8, (G_METHOD_NAMED | G_ARRAY) as i32)};
    
    // SPAGAIN
    // sp = my_perl.Istack_sp;
    // (PUTBACK)

    let res = stack_extract(&perl, cnt);

    // FREETMPS
    perl.free_tmps();
    // LEAVE
    unsafe_perl_api!{Perl_pop_scope(perl.my_perl)};
    
    Ok(res)
}

libperl-rs の試し方

現段階では github の master を試して頂くのが良いと思います。(crates.io にあるものは、YAPC Japan 2019 名古屋の 時のバージョンです。それ以後の改良部分を crates.io に送るのは、もう少し API が固まってからが良いのかなと…)

私自身の開発環境は Fedora 30 + System Perl (perl5.28) + System Rust (1.42) ですが、 travis-ci 上では Perl 公式 Docker イメージを用いて Perl 5.30〜5.22 での動作を確認してあります。 (発展的な例は threaded build な Perl5.26以上限定です) (libperl-sys までなら、もっと古い Perl でもビルド出来そうな気がします)

ですので、Debian, Ubuntu の上であれば以下の手順で動かしてみることは可能でしょう>

準備
    apt update &&
    apt install -y llvm-dev libclang-dev clang &&
    curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh &&
    sh rustup.sh -y &&
    source $HOME/.cargo/env &&
    rustup component add rustfmt

git clone https://github.com/hkoba/libperl-rs && cd libperl-rs
実行

examples/109_scan_subs_intro1.rs を実行してみる(渡したスクリプトの namespace から my (...) = @_ 形式の引数宣言だけを抜き出すサンプル)

% Z-chtholly(pts/2)% cargo run --example 109_scan_subs_intro1 -- -le '
package Int; 
sub foo { (my Int $x, my Int $y) = @_; $x + $y }
'
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/examples/109_scan_subs_intro1 -le '
package Int; 
sub foo { (my Int $x, my Int $y) = @_; $x + $y }
'`
$0 = "-e"
sub "foo"
preamble!
first array assignment from @_, lvalue = [PadNameType { name: Some("$x"), typ: Some("Int") }, PadNameType { name: Some("$y"), typ: Some("Int") }]

最後に

ここまで読んで下さり有難うございました。ご意見お待ちしております!

2022-03-30 追記: gist-it が死んでいてソースコード参照が動いていなかった箇所をコード埋め込みに変えました。