creating a `require`-able module
Yakiyo opened this issue · 8 comments
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.
Yes,
require
is quite flexible - you can just store yourhttp
table inpackaged.loaded["http"]
, andrequire
will find it.
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")
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.
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?
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).
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 howrlua
ensures Rust safety. What you can do is use create_registry_value to get back aRegistryKey
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.