Rust函数和API
本章前面三节介绍的是在Lua中定义的函数,本节来介绍在Rust中定义的函数。后续简单起见,分别称这两类函数为“Lua函数”和“Rust函数”。
其实我们已经接触过Rust函数了,在第一章hello, world!
版本的时候就已经支持的print()
就是Rust函数。当时的解释器就实现了Rust函数的定义和调用流程。其中定义如下:
pub enum Value {
RustFunction(fn (&mut ExeState) -> i32),
这里以print()
函数的实现代码为例:
fn lib_print(state: &mut ExeState) -> i32 {
println!("{}", state.stack[state.base + 1]);
0
}
而Rust函数的调用方法也跟Lua函数类似,也是在Call
字节码中调用Rust函数:
ByteCode::Call(func, _) => {
let func = &self.stack[func as usize];
if let Value::Function(f) = func {
f(self);
上面罗列的代码都是已经实现的Rust函数的功能,不过也只是最基本的定义和调用,还是缺少参数和返回值。本节为Rust函数增加这两个特性。
需要说明的一点是在Lua代码中,函数调用语句是不区分Lua函数和Rust函数的。换句话说,在语法分析阶段是不区分这两种类型的。只是在虚拟机执行阶段,才需要对两种类型区别处理。所以,本节下面介绍的都是虚拟机阶段。
参数
Rust函数的参数也是通过栈来传递的。
可以看到当前print()
函数的实现只支持一个参数,是通过直接读取栈上数据:state.stack[state.base + 1])
,其中self.base
是函数入口地址,+1
就是后面紧跟的地址,也就是第一个参数。
现在要支持多个参数,就要通知Rust函数具体的参数个数。有两个方案:
- 修改Rust函数原型定义,新增一个参数,表达参数的个数。这个方案实现简单,但是跟Lua官方的C函数原型不一致;
- 采用之前Lua函数中可变参数的机制,即通过栈顶位置来确定参数个数。
我们采取后面的方案。这需要在调用函数Rust前清理掉栈顶可能的临时变量:
ByteCode::Call(func, narg_plus) => {
let func = &self.stack[func as usize];
if let Value::Function(f) = func {
// narg_plus!=0,固定参数,需要清理栈顶可能的临时变量
// narg_plus==0,可变参数,无需清理
if narg_plus != 0 {
self.stack.truncate(self.base + narg_plus as usize - 1);
}
f(self);
在清理掉栈顶可能的临时变量后,在Rust函数中就可以通过栈顶来判断具体的参数个数了:state.stack.len() - state.base
;也可以直接读取任意的参数,比如第N个参数:state.stack[state.base + N])
。于是改造print()
函数如下:
fn lib_print(state: &mut ExeState) -> i32 {
let narg = state.stack.len() - state.base; // 参数个数
for i in 0 .. narg {
if i != 0 {
print!("\t");
}
print!("{}", state.stack[state.base + i]); // 打印第i个参数
}
println!("");
0
}
返回值
Rust函数的返回值也是通过栈来传递的。Rust函数在退出前把返回值放到栈顶,并返回数量,也就是Lua函数原型的i32
类型返回值的功能。这跟上一节介绍的Lua函数的机制一样。只需要在Call
字节码执行时,按照上一节中介绍的Lua函数返回值的方式来处理Rust函数的返回值即可:
ByteCode::Call(func, narg_plus) => {
let func = &self.stack[func as usize];
if let Value::Function(f) = func {
if narg_plus != 0 {
self.stack.truncate(self.base + narg_plus as usize - 1);
}
// 返回Rust函数返回值的个数,跟Lua函数一致
f(self) as usize
把Rust函数f()
的返回值从i32
转换为usize
类型并返回,表示返回值的个数。这里i32
到usize
的类型转换是扎眼的,这是因为Lua官方实现中C函数用返回负数来代表失败。我们到目前为止对所有的错误都是直接panic。后续章节会统一处理错误,届时使用Option<usize>
来代替i32
后,就会去掉这个扎眼的转换。
之前的print()
函数没有返回值,返回0
,所以并没有体现出返回值这个特性。下面用带返回值的另一个Lua标准库函数type()
举例。这个函数的功能是返回第一个参数的类型,返回值的类型是字符串,比如"nil"、"string"、"number"等。
fn lib_type(state: &mut ExeState) -> i32 {
let ty = state.stack[state.base + 1].ty(); // 第一个参数的类型
state.stack.push(ty); // 把结果压到栈上
1 // 只有1个返回值
}
这其中的ty()
函数是对Value
类型新增的方法,返回类型描述,这里省略具体代码。
Rust API
至此实现了Rust函数的参数和返回值的特性。但是上面对参数和返回值的访问和处理方式太过直接,给Rust函数的能力太强,不仅可以访问当前函数的参数,还可以方法整个栈空间,甚至整个state
状态。这是不合理的,也是危险的。需要限制Rust函数对state
状态的访问,包括整个栈,这就需要通过API来提供Rust函数访问state
的有限的能力。我们来到了一个新的世界:Rust API,当然在Lua官方实现中被称为C API。
Rust API是由Lua解释器提供的,给Rust函数(Rust实现的Lua库)使用的API。其角色如下:
+------------------+
| Lua代码 |
+---+----------+---+
| |
| +------V------+
| | 标准库(Rust)|
| +------+------+
| |
| |Rust API
| |
+---V----------V---+
| Lua虚拟机(Rust) |
+------------------+
上面小节中Rust函数中有3个功能需求就都应该由API来完成:
- 读取实际参数个数;
- 读取指定参数;
- 创建返回值
下面依次介绍这3个需求。首先是读取实际参数个数的功能,对应Lua官方实现中lua_gettop()
API。为此我们提供get_top()
API:
impl<'a> ExeState {
// 返回栈顶,即参数个数
pub fn get_top(&self) -> usize {
self.stack.len() - self.base
}
这个get_top()
函数虽然也是ExeState
结构的方法,但是是作为API提供给外部调用的。而ExeState
之前的方法(比如execute()
、get_stack()
等)都是虚拟机执行调用的内部方法。为了区分这两类方法,我们给ExeState
结构新增一个impl
块单独用来实现API,以增加可读性。只不过Rust中不允许在不同文件内实现结构体的方法,所以不能拆到另外一个文件中。
然后,读取指定参数的功能,这在Lua官方实现中并不是对应一个函数,而是一系列函数,比如lua_toboolean()
、lua_tolstring()
等,分别针对不同的类型。而借助Rust语言的泛型能力,我们就可以只提供一个API:
pub fn get<T>(&'a self, i: isize) -> T where T: From<&'a Value> {
let narg = self.get_top();
if i > 0 { // 正数索引,从self.base计数
let i = i as usize;
if i > narg {
panic!("invalid index: {i} {narg}");
}
(&self.stack[self.base + i - 1]).into()
} else if i < 0 { // 负数索引,从栈顶计数
let i = -i as usize;
if i > narg {
panic!("invalid index: -{i} {narg}");
}
(&self.stack[self.stack.len() - i]).into()
} else {
panic!("invalid 0 index");
}
}
可以看到这个API也支持负数索引,表示从栈顶开始倒数,这是Lua官方API的行为,也是很常见的使用方法。这也体现出API比直接访问栈的优势。
但是,这里也有跟官方API不一致的行为:当索引超出栈范围时,官方会返回nil
,但我们这里就直接panic。后续在介绍到错误处理时再详细讨论这里。
基于上述两个API,就可以重新print()
函数:
fn lib_print(state: &mut ExeState) -> i32 {
for i in 1 ..= state.get_top() {
if i != 1 {
print!("\t");
}
print!("{}", state.get::<&Value>(i).to_string());
}
println!("");
0
}
最后再来看最后一个功能,创建返回值。跟上面读取参数的API一样,在Lua官方实现里也对应一系列函数,比如lua_pushboolean()
、lua_pushlstring()
等。而这里也可以借助泛型只增加一个API:
pub fn push(&mut self, v: impl Into<Value>) {
self.stack.push(v.into());
}
基于这个API,上面type()
函数最后一行的self.stack.push()
就可以修改为self.push()
。
虽然替换API后,print()
和type()
函数的实现并没有明显变化,但是API对ExeState
提供了封装,在后续逐步增加库函数的过程中,会慢慢体现出方便性和安全性。