💎

syn crateを使ってproc_macroを書く (part.2 生成編)

2022/09/24に公開

part.1の続きです。

コードを生成する

必要なデータが取れるようになったので、コード生成部を書いていきます。最初に概形を示します。

#[proc_macro]
pub fn shiika_method_ref(input: TokenStream) -> TokenStream {
    let _spec = parse_macro_input!(input as ShiikaMethodRef);
    let mangled_name = "a";
    let function_definition = "b";
    let parameters = "c";
    let return_type = "d";
    let wrapper_name = "e";
    let args = "f";
    let gen = quote! {
        extern "C" {
            #[allow(improper_ctypes)]
            fn #mangled_name(#parameters) -> #return_type;
        }
        pub fn #wrapper_name(#parameters) -> #return_type {
            unsafe { #mangled_name(#args) }
        }
    };
    gen.into()
}

値の部分は仮のものを適当に埋めてあるので、これで実行すると

   Compiling skc_rustlib v0.1.0 (/Users/yhara/Dropbox/proj/shiika/lib/skc_rustlib)
error: expected item after attributes
  --> lib/skc_rustlib/src/sk_methods.rs:15:1
   |
15 | / shiika_method_ref!(
16 | |     "Meta:Class#new",
17 | |     fn(receiver: *const u8) -> SkClass,
18 | |     "meta_class_new"
19 | | );
   | |_^
   |
   = note: this error originates in the macro `shiika_method_ref` (in Nightly builds, run with -Z macro-backtrace for more info)

のようにエラーになります。

失敗したときにどのように展開したか教えてくれないのが不便ですが(-Z macro-backtraceは呼び出し履歴が出るだけ)、成功した場合の展開結果ならcargo-expandで見れるので、そのへんを使って頑張ります。

文字列リテラル→識別子

まず必要なのは"Meta_Class_new"というStringからRustの識別子を作る方法です。Stringをそのまま埋め込もうとすると

  fn "Meta_Class_new"(...)

のように不正なRustコードができてしまうためです。

syn::Identという型があるようなので、サンプルコードを参考にproc_macro2を入れます。

#[proc_macro]
pub fn shiika_method_ref(input: TokenStream) -> TokenStream {
    let spec = parse_macro_input!(input as ShiikaMethodRef);
    let mangled_name = Ident::new(
        &mangle_method(&spec.method_name.value()),
        Span::call_site());
    let parameters = &spec.parameters;
    let return_type = &spec.ret_ty;
    let wrapper_name = Ident::new(
        &spec.rust_func_name.value(),
        Span::call_site());
    let args = "todo";
    let gen = quote! {
        extern "C" {
            #[allow(improper_ctypes)]
            fn #mangled_name(#parameters) -> #return_type;
        }
        pub fn #wrapper_name(#parameters) -> #return_type {
            unsafe { #mangled_name(#args) }
        }
    };
    gen.into()
}

という定義をcargo expandすると

/Users/yhara/proj/shiika/lib/skc_rustlib % cargo expand sk_methods
   Compiling shiika_ffi_macro v0.1.0 (/Users/yhara/Dropbox/proj/shiika/lib/shiika_ffi_macro)
    Checking skc_rustlib v0.1.0 (/Users/yhara/Dropbox/proj/shiika/lib/skc_rustlib)
    Finished dev [unoptimized + debuginfo] target(s) in 0.82s

mod sk_methods {
    (略)
    extern "C" {
        #[allow(improper_ctypes)]
        fn Meta_Class_new(receiver: *const u8) -> SkClass;
    }
    pub fn meta_class_new(receiver: *const u8) -> SkClass {
        unsafe { Meta_Class_new("todo") }
    }
}

のように出力されました。かなり良い感じです。

メソッドにする

このままでもいいですが、Identを作る部分は以下のようにメソッドにしておくと:

impl ShiikaMethodRef {
    /// Returns mangled llvm func name (eg. `Meta_Class_new`)
    pub fn mangled_name(&self) -> Ident {
        Ident::new(
            &mangle_method(&self.method_name.value()),
            Span::call_site())
    }

    /// Returns user-specified func name. (eg. `meta_class_new`)
    pub fn wrapper_name(&self) -> Ident {
        Ident::new(
            &self.rust_func_name.value(),
            Span::call_site())
    }
}

呼び出し側がこのようにすっきりします。

#[proc_macro]
pub fn shiika_method_ref(input: TokenStream) -> TokenStream {
    let spec = parse_macro_input!(input as ShiikaMethodRef);
    let mangled_name = spec.mangled_name();
    let parameters = &spec.parameters;
    let return_type = &spec.ret_ty;
    let wrapper_name = spec.wrapper_name();
    let args = "todo";
    let gen = quote! {
        extern "C" {
            #[allow(improper_ctypes)]
            fn #mangled_name(#parameters) -> #return_type;
        }
        pub fn #wrapper_name(#parameters) -> #return_type {
            unsafe { #mangled_name(#args) }
        }
    };
    gen.into()
}

実引数部分の生成

最後に残ったのは、引数をフォワードする部分の生成です。ここでやりたいのは、

a: A, b: B

の変数名部分のみを抜き出して

a, b

という列を作ることです。

前者はPunctuated<Field, Token![,]>という型だったので、Punctuatedのリファレンスを見るとiterでイテレータが取れることがわかります。なのでmapしてFieldのidentを取ってやればとりあえずVec<Ident>を作ることはできそうです。

Vecを埋め込む

ただしVec<Ident>はそのままではRustソースに埋め込むことはできません。何区切りかが不明確だからですね。今回はカンマで区切って並べたいのでどうするかというと…、Punctuatedが使えそうな予感がします。ということでShiikaMethodRefに以下のようなメソッドを生やすとうまくいきました。

    /// Returns list of parameters for forwarding function call (eg. `a, b, c`)
    pub fn forwaring_args(&self) -> Punctuated<Ident, Token![,]> {
        self.parameters.iter().map(|field| {
            field.ident.clone().expect("Field::ident is None. why?")
        }).collect()
    }

完成!

ということでできたものがこちらです。お疲れさまでした。

https://github.com/shiika-lang/shiika/pull/403

Discussion