Rust Function and APIs

The previous three sections of this chapter introduce the functions defined in Lua, and this section introduces the functions defined in Rust. For the sake of simplicity, these two types of functions are called "Lua functions" and "Rust functions" respectively.

In fact, we have already been exposed to Rust functions. The print() that was supported in the hello, world! version of the first chapter is the Rust function. The interpreter at that time realized the definition and calling process of Rust functions. which is defined as follows:

pub enum Value {
     RustFunction(fn (&mut ExeState) -> i32),

Here is an example of the implementation code of print() function:

fn lib_print(state: &mut ExeState) -> i32 {
     println!("{}", state.stack[state.base + 1]);
     0
}

The calling method of the Rust function is also similar to the Lua function, and the Rust function is also called in the Call bytecode:

     ByteCode::Call(func, _) => {
         let func = &self. stack[func as usize];
         if let Value::Function(f) = func {
             f(self);

The codes listed above are the functions of the implemented Rust functions, but they are only the most basic definitions and calls, and still lack parameters and return values. This section adds these two features to Rust functions.

One thing that needs to be explained is that in Lua code, the function call statement does not distinguish between Lua functions and Rust functions. In other words, the two types are not distinguished during the parsing phase. It is only in the virtual machine execution stage that the two types need to be treated differently. Therefore, what is described below in this section is all about the virtual machine stage.

Argument

The arguments of Rust functions are also passed through the stack.

You can see that the implementation of the current print() function only supports one parameter, which is by directly reading the data on the stack: state.stack[state.base + 1]), where self.base is the function entry address , +1 is the address immediately following, that is, the first parameter.

Now to support multiple parameters, it is necessary to inform the Rust function of the specific number of parameters. There are two options:

  • Modify the Rust function prototype definition, add a parameter to express the number of parameters. This solution is simple to implement, but it is inconsistent with Lua's official C function prototype;
  • Adopt the variable parameter mechanism in the previous Lua function, that is, determine the number of parameters by the position of the top of the stack.

We take the latter approach. This requires cleaning up possible temporary variables on the top of the stack before calling the function Rust:

     ByteCode::Call(func, narg_plus) => {
         let func = &self. stack[func as usize];
         if let Value::Function(f) = func {
             // narg_plus!=0, fixed parameters, need to clean up possible
             //               temporary variables on the top of the stack;
             // narg_plus==0, variable parameters, no need to clean up.
             if narg_plus != 0 {
                 self.stack.truncate(self.base + narg_plus as usize - 1);
             }

             f(self);

After cleaning up the possible temporary variables at the top of the stack, in the Rust function, the specific number of parameters can be judged through the top of the stack: state.stack.len() - state.base; we can also directly read any argument, such as the Nth parameter: state.stack[state.base + N]). So modify the print() function as follows:

fn lib_print(state: &mut ExeState) -> i32 {
     let narg = state.stack.len() - state.base; // number of arguments
     for i in 0 .. narg {
         if i != 0 {
             print!("\t");
         }
         print!("{}", state.stack[state.base + i]); // print the i-th argument
     }
     println!("");
     0
}

Return Value

The return value of the Rust function is also passed through the stack. The Rust function puts the return value on the top of the stack before exiting, and returns the number, which is the function of the i32 type return value of the Lua function prototype. This is the same mechanism as the Lua function introduced in the previous section. We only need to process the return value of the Rust function according to the return value of the Lua function introduced in the previous section when the Call bytecode is executed:

     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);
             }

             // Return the number of return values of the Rust function,
             // which is consistent with the Lua function
             f(self) as usize

Convert the return value of the Rust function f() from i32 to usize type and return, indicating the number of return values. Here the type conversion from i32 to usize feels bad, because the C function in the official Lua implementation returns a negative number to indicate failure. We have directly panicked on all errors so far. Subsequent chapters will deal with errors uniformly. When Option<usize> is used instead of i32, this garish conversion will be removed.

The previous print() function had no return value and returned 0, so it did not reflect the feature of return value. Let's take another Lua standard library function type() with a return value as an example. The function of this function is to return the type of the first parameter, and the type of the return value is a string, such as "nil", "string", "number" and so on.

fn lib_type(state: &mut ExeState) -> i32 {
     let ty = state.stack[state.base + 1].ty(); // the type of the first parameter
     state.stack.push(ty); // Push the result onto the stack
     1 // Only 1 return value
}

Among them, the ty() function is a new method for the Value type, which returns a description of the type, and the specific code is omitted here.

Rust API

So far, the characteristics of the parameters and return values of Rust functions have been realized. However, the access and processing of arguments and return values above are too direct, and the ability of Rust functions is too strong, not only can access the parameters of the current function, but also can access the entire stack space, and even the entire state state. This is irrational and dangerous. It is necessary to restrict the access of Rust functions to state, including the entire stack, which requires the limited ability of Rust functions to access state through the API. We have come to a new world: the Rust API, of course called the C API in the official Lua implementation.

The Rust API is an API provided by the Lua interpreter for Rust functions (the Lua library implemented by Rust). Its roles are as follows:

     +------------------+
     |     Lua code     |
     +---+----------+---+
         |          |
         |  +-------V----------+
         |  | Standard Library |
         |  |    (Rust)        |
         |  +-------+----------+
         |          |Rust API
         |          |
+--------V----------V--------+
| Lua Virtual Machine (Rust) |
+----------------------------+

There are 3 functional requirements in the Rust function in the above section, all of which should be fulfilled by the API:

  • Read the actual number of arguments;
  • read specified argument;
  • create return value

These three requirements are described in turn below. The first is the function of reading the actual number of arguments, which corresponds to the lua_gettop() API in the official implementation of Lua. For this we provide get_top() API:

impl<'a> ExeState {
     // Return to the top of the stack, that is, the number of parameters
     pub fn get_top(&self) -> usize {
         self.stack.len() - self.base
     }

Although the get_top() function is also a method of the ExeState structure, it is provided as an API for external calls. The methods before ExeState (such as execute(), get_stack(), etc.) are all internal methods for virtual machine execution calls. In order to distinguish these two types of methods, we add an impl block to the ExeState structure to implement the API alone to increase readability. It's just that Rust does not allow the method of implementing the structure in different files, so it cannot be split into another file.

Then, the function of reading the specified parameters does not correspond to a function in the official Lua implementation, but a series of functions, such as lua_toboolean(), lua_tolstring(), etc., for different types. With the generic capabilities of the Rust language, we can provide only one API:

     pub fn get<T>(&'a self, i: isize) -> T where T: From<&'a Value> {
         let narg = self. get_top();
         if i > 0 { // positive index, counting from 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 { // Negative index, counting from the top of the stack
             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");
         }
     }

You can see that this API also supports negative indexes, which means counting down from the top of the stack, which is the behavior of Lua's official API, and it is also a very common method of use. This also reflects the advantages of the API over direct access to the stack.

However, there is also a behavior that is inconsistent with the official API: when the index exceeds the stack range, the official will return nil, but here we panic directly. We will discuss this in detail later when we introduce error handling.

Based on the above two APIs, you can redo the print() function:

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
}

Finally, let's look at the last function, creating the return value. Like the above API for reading arguments, there are also a series of functions in the official Lua implementation, such as lua_pushboolean(), lua_pushlstring(), etc. And here you can also add only one API with the help of generics:

     pub fn push(&mut self, v: impl Into<Value>) {
         self.stack.push(v.into());
     }

Based on this API, self.stack.push() in the last line of type() function above can be changed to self.push().

Although the implementation of the print() and type() functions has not changed significantly after replacing the API, the API provides a encapsulation for ExeState, which will gradually reflect the convenience in the process of gradually adding library functions safety.