mlua-rs/rlua

creating a `require`-able module

Yakiyo opened this issue · 8 comments

Yakiyo commented

Hi i want to declare a lua module from the rust side that i can require("lib"). This is what i tried for a http module

use reqwest::blocking::Client;
use rlua::{Context, Lua, Result as LuaResult, UserData, UserDataMethods};
use std::{ops::Deref, sync::Arc};

/// An http client
pub struct HttpClient(Client);

/// Http response
pub struct HttpResponse {
    pub body: String,
    pub code: u16,
    // pub headers:
}

impl From<reqwest::blocking::Response> for HttpResponse {
    fn from(value: reqwest::blocking::Response) -> Self {
        HttpResponse {
            // TODO: should not unwrap here tbh, but oh well. Should fix this in the future
            code: value.status().as_u16(),
            body: value.text().unwrap(),
        }
    }
}

impl UserData for HttpResponse {}

impl Deref for HttpClient {
    type Target = Client;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn client_get(_: rlua::Context<'_>, client: &HttpClient, url: String) -> LuaResult<HttpResponse> {
    let response: HttpResponse = client
        .get(&url)
        .send()
        .map_err(|e| rlua::Error::ExternalError(Arc::new(e)))?
        .into();
    Ok(response)
}

/// Create a new client instantiation
pub fn create_client(_: Context, _: ()) -> LuaResult<HttpClient> {
    Ok(HttpClient(Client::new()))
}

impl UserData for HttpClient {
    fn add_methods<'lua, T: UserDataMethods<'lua, Self>>(methods: &mut T) {
        methods.add_method("get", client_get);

        methods.add_method("post", |_, client, url: String| {
            let response: HttpResponse = client
                .post(&url)
                .send()
                .map_err(|e| rlua::Error::ExternalError(Arc::new(e)))?
                .into();
            Ok(response)
        });
    }
}

/// Load the http module within the lua context
pub fn load(lua: &Lua) -> LuaResult<()> {
    lua.context::<_, LuaResult<()>>(|ctx| {
        let http_module = ctx.create_table()?;
        // Register the `http.client()` function
        http_module.set("client", ctx.create_function(create_client)?)?;
        // Register the `http.get()` function. This internally creates a new client
        // and invokes its `get` method
        http_module.set(
            "get",
            ctx.create_function(|_, url: String| {
                let client = HttpClient(Client::new());
                let response: HttpResponse = client
                    .get(&url)
                    .send()
                    .map_err(|e| rlua::Error::ExternalError(Arc::new(e)))?
                    .into();
                Ok(response)
            })?,
        )?;
        let globals = ctx.globals();
        globals.set("http", http_module)?;
        Ok(())
    })
}

#[cfg(test)]
mod tests {
    use rlua::*;
    #[test]
    fn lua_http() {
        let lua = Lua::new();
        super::load(&lua).unwrap();
        lua.context(|ctx| {
            ctx.load(r#"
            local response_get = http.get("https://example.com")

            print(response_get)
            "#).exec().unwrap();
        })
    }
}

here the http var is globaly available as a variable. Is there no way to make it so that i can access it with a require instead of having it as a variable?

Yes, require is quite flexible - you can just store your http table in packaged.loaded["http"], and require will find it.

https://www.lua.org/manual/5.4/manual.html#pdf-require

Yakiyo commented

Yes, require is quite flexible - you can just store your http table in packaged.loaded["http"], and require will find it.

https://www.lua.org/manual/5.4/manual.html#pdf-require

How do i go about implementing it? i tried the following:

let globals = ctx.globals();
let packaged: rlua::Table = globals.get("packaged").unwrap_or(ctx.create_table()?);
let loaded: rlua::Table = packaged.get("loaded").unwrap_or(ctx.create_table()?);
loaded.set("http", http)?;
packaged.set("loaded", loaded)?;
globals.set("packaged", packaged)?;

yet when running the following snippet

http = require("http")

-- define client
local client = http.client()

-- client get request
local res = client:get("https://httpbin.org/get")
print("res 1")
print(res:body())

-- client post request
local res2 = client:post("https://httpbin.org/post", "hi")
print("res 2")
print(res2:body())

-- client do_request
local request = http.request("GET", "https://httpbin.org/get")
local res3 = client:do_request(request)
print("res 3")
print(res3:body())

print("status equality")
print(res:status() == res3:status())

-- top level get function 
-- equivalent to creating a client and then doing a get request
local get_resp = http.get("https://httpbin.org/get")
print("get resp")
print(get_resp:body())

i get this error

RuntimeError("[string \"?\"]:1: module 'http' not found:\n\tno field package.preload['http']\n\tno file '/usr/local/share/lua/5.4/http.lua'\n\tno file '/usr/local/share/lua/5.4/http/init.lua'\n\tno file '/usr/local/lib/lua/5.4/http.lua'\n\tno file '/usr/local/lib/lua/5.4/http/init.lua'\n\tno file './http.lua'\n\tno file './http/init.lua'\nstack traceback:\n\t[C]: in ?\n\t[C]: in function 'require'\n\t[string \"?\"]:1: in main chunk")
azdle commented

You've (both) got a small typo, you're meaning to use package.loaded (not 'packaged').

Though, that is technically not the way you're supposed to do it. As https://www.lua.org/manual/5.4/manual.html#pdf-package.loaded says:

This variable is only a reference to the real table; assignments to this variable do not change the table used by require. The real table is stored in the C registry (see §4.3), indexed by the key LUA_LOADED_TABLE, a string.

(Though, in my experience, despite what it says, it does (at least sometimes) work.)

The proper™ way to make a custom set of packages for require is to create a package.searchers entry: https://www.lua.org/manual/5.4/manual.html#pdf-package.searchers

Oops, thanks @azdle for the correction, I did indeed mean package.loaded.

@Yakiyo you don't need to re-set packaged/loaded, you're working with references to the original table. Here's a brief working example:

    Lua::new().context(|lua| {
        let my_mod = lua.create_table().unwrap();
        let hello = lua.create_function(|_, (): ()| {
            println!("Hello, world!");

            Ok(())
        }).unwrap();
        my_mod.set("hello", hello).unwrap();

        let globals = lua.globals();
        let package: Table<'_> = globals.get("package").unwrap();
        let package_loaded: Table<'_> = package.get("loaded").unwrap();
        package_loaded.set("my_mod", my_mod).unwrap();

        let () = lua.load(
            r#"
               local my_mod = require('my_mod')
               my_mod.hello()
            "#,
            )
            .eval()
            .unwrap();
    });

If you're the code setting up the application, and you're happy to build your modules immediately (i.e. it's not expensive), then I don't see any reason not to do it this way. If you let random Lua code run before then, then package.loaded may have been changed to point to something completely different.

I agree that accessing it via the registry lua.named_registry_value(rlua::ffi::LUA_LOADED_TABLE) would be more correct, though ffi is currently private in rlua. You could assume it won't change (I don't think it can change within a release), so you could get away with using b"_LOADED\0" (the value in Lua 5.3/5.4).

Using package.searchers is probably better for more involved cases, e.g. if you have many modules or some are expensive to initialise, but IMO is overkill when adding a small module or three in a small application.

Yakiyo commented

Oops, thanks @azdle for the correction, I did indeed mean package.loaded.

@Yakiyo you don't need to re-set packaged/loaded, you're working with references to the original table.

yeah i figured that, i initially did it cz since packaged was nil, i thought i had to reset it. using package.loaded works fine for me. i was just trying to write some extension modules that can be used in rlua (repo). i dont suppose there could be a way to use package.searchers for that. But just for reference, how would u do that with package.searchers? whats the function signature for it?

Yakiyo commented

And another question. i have a lua file, which contains 3/4 different functions. is it in any way possible for me to take and call those functions from the rust side, outside of Lua::context? the premise of this is, ive got extensions written in lua, where all of them export, for example two funcs, add and sub. i'll load each lua file with a new Lua instance, and then eval them? but then how do i keep a reference to those funcs, so that i can invoke em when i need. instead of calling lua.context and doing something else.

But just for reference, how would u do that with package.searchers? whats the function signature for it?

According to https://www.lua.org/manual/5.4/manual.html#pdf-package.searchers the searcher is called with the module name, and if successful returns a "loader" function; calling that loader function returns the module. Which sounds complicated for one module, but makes a lot of sense if the searcher is doing something like looking in the filesystem, and returns a function to read and compile that file.

but then how do i keep a reference to those funcs, so that i can invoke em when i need. instead of calling lua.context and doing something else.

You can't call the Lua functions outside of lua.context - this is part of how rlua ensures Rust safety. What you can do is use create_registry_value to get back a RegistryKey which you can store outside of the context function. You can then get the Lua value back efficiently with registry_value later (but still in the context call).

Yakiyo commented

But just for reference, how would u do that with package.searchers? whats the function signature for it?

According to https://www.lua.org/manual/5.4/manual.html#pdf-package.searchers the searcher is called with the module name, and if successful returns a "loader" function; calling that loader function returns the module. Which sounds complicated for one module, but makes a lot of sense if the searcher is doing something like looking in the filesystem, and returns a function to read and compile that file.

but then how do i keep a reference to those funcs, so that i can invoke em when i need. instead of calling lua.context and doing something else.

You can't call the Lua functions outside of lua.context - this is part of how rlua ensures Rust safety. What you can do is use create_registry_value to get back a RegistryKey which you can store outside of the context function. You can then get the Lua value back efficiently with registry_value later (but still in the context call).

Aight understood. thanks for the help. then ill go ahead and close this as completed.