Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rustで作るtree-sitterパーサーのRubyバインディング

Tomohiro Hashidate
August 24, 2024
880

 Rustで作るtree-sitterパーサーのRubyバインディング

大阪Ruby会議04

Tomohiro Hashidate

August 24, 2024
Tweet

Transcript

  1. require "tree_house" require "rouge" TreeHouse.register_lang("ruby", "./libtree-sitter-ruby.so") parser = TreeHouse::Parser.new parser.set_language("ruby")

    source = File.read("./sample.rb") puts "== Source ==" puts source puts "\n" puts "== Tree ==" tree = parser.parse(source) puts tree.root_node.to_sexp
  2. Cargo.toml in workspace [package] name = "gem_name" version = "0.1.0"

    edition = "2021" authors = ["joker1007 <[email protected]>"] publish = false [lib] crate-type = ["cdylib"] [dependencies] magnus = { version = "0.6.2" } # 雛形は最新ではない場合があるので更新推奨
  3. magnusのエントリポイント magnus::initというAttribute Macroを利用する。 #[magnus::init] fn init(ruby: &Ruby) -> Result<(), Error>

    { let namespace = ruby.define_module("TreeHouse")?; namespace.define_singleton_method("register_lang", function!(register_lang, 2))?; namespace.define_singleton_method("available_langs", function!(available_langs, 0))?; // ... let point_class = namespace.define_class("Point", ruby.class_object())?; point_class.define_singleton_method("new", function!(data::Point::new, 2))?; point_class.define_method("hash", method!(<data::Point as typed_data::Hash>::hash, 0))?; point_class.define_method("==", method!(<data::Point as typed_data::IsEql>::is_eql, 1))?; point_class.define_method( "eql?", method!(<data::Point as typed_data::IsEql>::is_eql, 1), )?; }
  4. Rustの構造体をRubyのクラスとして利用する #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[magnus::wrap(class

    = "TreeHouse::Point", free_immediately)] // wrapマクロが構造体に必要な変換処理を自動実装する pub struct Point { pub row: usize, pub column: usize, } impl Point { pub fn new(row: usize, column: usize) -> Self { Self { row, column } } pub fn inspect(&self) -> String { format!("#<Point({}, {})>", self.row, self.column) } pub fn to_s(&self) -> String { format!("({}, {})", self.row, self.column) } pub fn get_row(&self) -> usize { self.row } pub fn get_column(&self) -> usize { self.column } }
  5. メソッドの割り当て // hash, is_eqlはマクロが自動実装している let point_class = namespace.define_class("Point", ruby.class_object())?; point_class.define_singleton_method("new",

    function!(data::Point::new, 2))?; point_class.define_method("hash", method!(<data::Point as typed_data::Hash>::hash, 0))?; point_class.define_method("==", method!(<data::Point as typed_data::IsEql>::is_eql, 1))?; point_class.define_method( "eql?", method!(<data::Point as typed_data::IsEql>::is_eql, 1), )?; point_class.define_method("row", method!(data::Point::get_row, 0))?; point_class.define_method("column", method!(data::Point::get_column, 0))?; method! マクロが、メソッドのシグネチャを見てRubyのValueとして変換可能である かを検証し、define_methodの引数として適した形に変換してくれる。
  6. RustがRubyから受け取れるもの 基本的にRust側で受け取れるのは以下の通り。 複製が容易な基本型(i32, u32, String etc.) TypedDataに変換可能な型(wrapマクロを付与したstruct)への参照型 上記の型の実体を typed_data::Obj でラップしたもの、これはラップしてい

    る型自体がポインタになっているので全体としては参照を意味する RubyのValueをラップした型(RString, RArrayなど) 汎用Value型 ライブラリの観点から言うとTryConvertトレイトを実装している型
  7. オブジェクト自体を返したり、RubyのAPIを使いたい use magnus::typed_data::Obj; fn sample2(ruby: &Ruby, rb_self: Obj<Self>) -> Obj<Self>

    第一引数 &Ruby 型にして、第二引数をselfのRubyオブジェクト表現にすると、 magnusで定義されているRuby API呼び出しのための参照が得られる。 &Ruby 型を経由すると、新しいクラスを定義したり、メソッドがブロックを受け取っ ているか調べたりできる。 ObjはRTypeDataとして扱えるstructへのポインタを示す型。 メソッドの戻り値として使えて、更にDeRefトレイトによりstructそのものと同じ様に メソッドが呼べる様になっている。 また、funcallやivar_setなどRubyオブジェクトとして扱いたい時のためのメソッドも 生えてて便利。
  8. magnus::value::Lazyを使う magnusライブラリはそういう用途をちゃんと考慮している。 staticに割り当てておいて、必要になった時点でValueとして表現可能な構造体への参 照に変換できる型が用意されている。 Lazyを利用するとさっきのコードはこうなる。 static STRING_CLASS: Lazy<RClass> = Lazy::new(|ruby|

    RClass::from_value(eval("String").unwrap()).unwrap()); #[magnus::init] fn init(ruby: &Ruby) { Lazy::force(&STRING_CLASS, ruby); // requireされた時にRubyの情報が取れるので、その時にLazyの内容を確定させる } fn build_string(ruby: &Ruby, rb_self: Value) -> String { let string_class = STRING_CLASS.get_inner_with(ruby) string_class.new_instance(()).unwrap() }
  9. Enumeratorのサンプル pub fn children<'cursor>( ruby: &Ruby, rb_self: typed_data::Obj<Self>, ) ->

    Result<Yield<impl Iterator<Item = Node<'tree>>>, Error> { let mut cursor = rb_self.raw_node.walk(); let nodes = rb_self.raw_node.children(&mut cursor); let array = ruby.ary_new_capa(nodes.len()); for n in nodes { let node = Self { raw_tree: Arc::clone(&rb_self.raw_tree), raw_node: n, }; array.push(node)? } array.freeze(); if ruby.block_given() { Ok(Yield::Iter(array.into_iter())) } else { Ok(Yield::Enumerator(rb_self.enumeratorize("children", ()))) } }
  10. ハマリポイント2: GCからの保護 parser = TreeHouse::Parser.new.tap do |p| p.set_language("ruby") end node

    = parser.parse(source).root_node node.child(0).child(0) GC.start node.child(0).child(0) # => die 最初に何も考えずに実装した時は、このコードはSegmentation Faultを起こした。 どうしてでしょうか?
  11. 初期実装 pub struct Tree { raw_tree: tree_sitter::Tree, } pub struct

    Node<'tree> { pub raw_node: tree_sitter::Node<'tree>, } シンプルにRustライブラリが返すstructをwrapしていた。 Rustのライフタイム表現でNodeはTreeより長生きできないことが分かるが、それは RubyのGCの動きとは関係がない。 structから直接参照できる範囲にTreeが無いので、Rubyの世界ではオブジェクト同士 の関係も分からない。
  12. 改修後 pub struct Tree { raw_tree: Arc<tree_sitter::Tree>, } pub struct

    Node<'tree> { pub raw_tree: Arc<tree_sitter::Tree>, pub raw_node: tree_sitter::Node<'tree>, } ArcはRustでAtomicなReference Countを実現するためのスマートポインタと呼ばれて いる型。 Arcを利用して、TreeオブジェクトからNodeの参照を取り出す時に、Nodeが全てGC されてReference Countが0にならない限り、参照先のTreeがメモリから開放されない 様にした。 (ドキュメントにはBoxValueというGCからオブジェクトを保護するための型があった が、制約が色々あって利用できなかった)
  13. use libloading::Library; fn register_lang(lang: String, path: String) -> () {

    let func_name = String::from("tree_sitter_") + &lang; let libraries = LANG_LIBRARIES.get_or_init(|| Mutex::new(HashMap::new())); let languages = LANG_LANGUAGES.get_or_init(|| Mutex::new(HashMap::new())); unsafe { let mut libraries = libraries.lock().unwrap(); let lib = libraries.entry(lang.to_string()).or_insert_with(|| { let loaded = Library::new(path).expect("Failed to load library"); loaded }); let func: libloading::Symbol<unsafe extern "C" fn() -> *const TSLanguage> = lib.get(func_name.as_bytes()).unwrap(); language = tree_sitter::Language::from_raw(func()); let mut languages = languages.lock().unwrap(); languages.insert(lang.to_string(), language); }; }