加一个原生调用

这是让用户在 lib/ 里调到一个函数、而编译出来是一个节点的做法。lib/ 里的原生函数没有实现 —— 属性宏会把函数体换成 unreachable!(),只留下签名当声明:
impl Entity {
    #[native_calc(73)]
    pub fn current() -> Self;

    #[native_exec(70)]
    pub fn spawn_preset(id: Guid, list: List<i32>);
}
用这些属性的 crate 需要两行 crate 属性,详见下面的「编译器怎么认出这是原生调用」:
#![feature(register_tool)]
#![register_tool(rust2genshin)]
宏还会加上 #[inline(never)] 和 #[rustc_no_mir_inline],避免这个声明被内联掉、调用点消失。

三个宏的区别

前两个只差一个布尔:编译器直接按 Rust 签名合成节点 NodeKind::simple(id, control, control, 入参类型, 出参类型)。所以不需要先在 core/src/node/ 里声明这个节点,把 ID 写对就行。

#[native("名字")] 为什么要改 core

字符串形式用于签名不足以描述节点的情况(类型相关、或参数/返回值不是直通)。它需要在 core/src/compile/native.rs 里加分支:例外是 PartialMut 的三个内建(partial_mut_from / partial_mut_deref / partial_mut_deref_mut):它们在 compile_call 里特判,不走 compile_native_call。原因是 &mut T 在 IR 里是个元组 (LocalSetter, PartialMut<T>),partial_mut_from 要把它拆开取读槽,而不是值直通。类型声明的写法是把一个不透明的外部类型挂上属性:
unsafe extern "Rust" {
    #[native("Entity")]
    pub(crate) type EntityInternal;
}

泛型原生类型

List 是泛型原生类型:属性标在 struct List<T> 的定义上,元素类型 T 从调用点的泛型实参里取:
#[native("List")]
pub struct List<T> { /* 字段在节点图里不存在 */ }
编译器把它编成 IR 层的 IrKind::List(T)(元素也是 IR 类型),降级时才落成 NativeKind::List。因此 [i32; N] 和 List<i32> 是同一个类型,两者之间的 transmute 是空转换 —— 数组字面量可以直接当列表用。元素类型必须是节点图里有列表变体的原生类型(整数、字符串、实体、GUID、浮点、三维向量、布尔、配置、预制件、阵营);结构体、枚举、嵌套列表都不行,会报 Unsupported list element type。原生类型(句柄)的析构是空操作:它的字段在节点图里并不存在,析构胶水去读字段只会读到不存在的东西。

限制

入参和返回值的类型都必须能变成 NativeKind —— 编译器在这两处直接 .unwrap(),遇到转不了的类型会 panic。所以原生函数不能收任意类型。

编译器怎么认出这是原生调用

属性宏除了把函数体换成 unreachable!(),还会发一条 tool attribute:
#[rust2genshin::call("list_len")]
#[allow(unused_variables)]
#[inline(never)]
#[rustc_no_mir_inline]
pub fn len(&self) -> usize;
register_tool 注册的属性编译器不会消耗它 —— 它原样留在 HIR 里, 后端用 tcx.get_all_attrs() 直接读那个定义上的属性,拿到名字/节点 ID。用宏的 crate 要有这两行:
#![feature(register_tool)]
#![register_tool(rust2genshin)]
少了两行,编译器会报「cannot find crate rust2genshin」—— 那个提示看不出 要干什么,看到它就来加这两行。

为什么不从源码里抠

早先的做法是:属性宏把 token 的 span 设成 Span::call_site(),编译器再用 span_to_snippet 把宏属性的原文从源码里读回来、重新 parse。那套办法有三个问题:换成 tool attribute 之后,属性是结构化的,直接从 tcx 里读,不碰源码。 core 也因此不再依赖 syn。宏仍然必须留着:tool attribute 不移除函数体,而 impl 块里不能写无体函数。

tool attribute 的五个档

| 属性 | 参数 | 用途 | |
|
|
| | rust2genshin::call("名字") | 字符串 | 签名不足以描述节点,按名字查(要改 core) | | rust2genshin::calc(N) | 整数 | 无控制流的节点 | | rust2genshin::exec(N) | 整数 | 有控制流的节点 | | rust2genshin::ty("名字") | 字符串 | 标在类型上:原生类型 | | rust2genshin::enumty(N) | 整数 | 标在枚举上:#[native_enum(N)] |两个例外:rust2genshin::event(N) 标在事件结构体上,rust2genshin::listener 标在 #[event_listener] 的函数上(无参数)。enumty 而不是 enum,因为 enum 是关键字,不能当路径段。

事件:#[event(N)] 标在 struct 上

事件类型是同一个家族的另一支,但它标的是 struct 而不是函数:
#[event(307)]
pub struct TabSelectedEvent {
    pub entity: Entity,
    pub guid: Guid,
    pub index: i32,
    // ...
}
编译器侧的落点在 core/src/compile/mod.rs 的 run():遍历带 #[event_listener] 的入口时,取出第一个参数的类型的 #[event(N)],把字段类型拼成出参,然后在主图里插入 trigger 节点和对应的复合节点调用块。

相关

#[event_listener] 也是同一套机制的特例:它除了改写 span,还会自动给函数加上 #[unsafe(no_mangle)],把它变成导出的入口。详见事件。