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类型并返回,表示返回值的个数。这里i32usize的类型转换是扎眼的,这是因为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提供了封装,在后续逐步增加库函数的过程中,会慢慢体现出方便性和安全性。