Compare commits

...

2 Commits

Author SHA1 Message Date
94e1cf2eb0
Working commit 2025-06-20 11:21:38 +10:00
f9676a1436
Automatically generate drop for #[metatype] 2025-06-19 23:07:53 +10:00
11 changed files with 1644 additions and 17 deletions

1307
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
[workspace] [workspace]
members = [ members = [
"crates/lb_core",
"crates/luaffi", "crates/luaffi",
"crates/luaffi_impl", "crates/luaffi_impl",
"crates/luaify", "crates/luaify",
@ -12,4 +13,13 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
luajit = { version = "0.1.0", path = "crates/luajit" } clap = { version = "4.5.40", features = ["derive"] }
console-subscriber = "0.4.1"
lb_core = { version = "0.1.0", path = "crates/lb_core" }
luaffi = { version = "0.1.0", path = "crates/luaffi" }
luajit = { version = "0.1.0", path = "crates/luajit", features = ["runtime"] }
mimalloc = "0.1.47"
owo-colors = "4.2.1"
tokio = { version = "1.45.1", features = ["full", "tracing"] }
tracing = "0.1.41"
tracing-subscriber = "0.3.19"

10
crates/lb_core/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "lb_core"
version = "0.1.0"
edition = "2024"
[dependencies]
luaffi = { version = "0.1.0", path = "../luaffi" }
luajit = { version = "0.1.0", path = "../luajit" }
owo-colors = "4.2.1"
tokio = { version = "1.45.1", features = ["full"] }

92
crates/lb_core/src/lib.rs Normal file
View File

@ -0,0 +1,92 @@
use luaffi::{cdef, metatype};
use luajit::State;
use owo_colors::OwoColorize;
use std::{cell::RefCell, fmt, process};
#[derive(Debug)]
pub struct GlobalState(State);
impl GlobalState {
thread_local! {
static STATE: RefCell<Option<GlobalState>> = RefCell::new(None);
}
pub fn set(state: State) -> Option<State> {
Self::STATE.with_borrow_mut(|s| s.replace(Self(state)).map(|s| s.0))
}
pub fn with_current<T>(f: impl FnOnce(&State) -> T) -> T {
Self::STATE.with_borrow(|s| f(&s.as_ref().expect("lua state not initialised").0))
}
pub fn with_current_mut<T>(f: impl FnOnce(&mut State) -> T) -> T {
Self::STATE.with_borrow_mut(|s| f(&mut s.as_mut().expect("lua state not initialised").0))
}
pub fn new_thread() -> State {
Self::with_current(|s| s.new_thread())
}
pub fn uncaught_error(err: luajit::Error) {
let mut err = PrettyError::from(err);
if let Some(task) = tokio::task::try_id() {
err.prepend(format_args!("uncaught error in task {task}"));
}
eprintln!("{err}");
process::abort()
}
}
#[derive(Debug, Clone)]
pub struct PrettyError {
msg: String,
trace: Option<String>,
}
impl PrettyError {
pub fn new(msg: impl fmt::Display) -> Self {
Self {
msg: format!("{msg}"),
trace: None,
}
}
pub fn with_trace(mut self, trace: impl fmt::Display) -> Self {
self.trace = Some(format!("{trace}"));
self
}
pub fn prepend(&mut self, msg: impl fmt::Display) -> &mut Self {
if self.msg.is_empty() {
self.msg = format!("{msg}");
} else {
self.msg = format!("{msg}:\n{}", self.msg);
}
self
}
}
impl From<luajit::Error> for PrettyError {
fn from(value: luajit::Error) -> Self {
match value {
luajit::Error::Resume { msg, trace } => Self::new(msg).with_trace(trace),
err => Self::new(err),
}
}
}
impl fmt::Display for PrettyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.trace {
Some(ref trace) => write!(f, "{}\n{trace}", self.msg.red()),
None => write!(f, "{}", self.msg.red()),
}
}
}
#[cdef]
pub struct lb_core;
#[metatype]
impl lb_core {}

View File

@ -32,7 +32,7 @@ unsafe extern "C" fn __is_utf8(ptr: *const u8, len: usize) -> bool {
} }
const CACHE_LIBS: &[(&str, &str)] = &[ const CACHE_LIBS: &[(&str, &str)] = &[
// preloaded // libs in global
("table", "table"), ("table", "table"),
("string", "string"), ("string", "string"),
("math", "math"), ("math", "math"),
@ -143,13 +143,18 @@ impl Registry {
} }
pub fn declare<T: Type>(&mut self, name: impl Display) -> &mut Self { pub fn declare<T: Type>(&mut self, name: impl Display) -> &mut Self {
self.include::<T>(); self.include::<T>()
self.funcs .funcs
.insert(name.to_string()) .insert(name.to_string())
.then(|| writeln!(self.cdef, "{};", T::cdecl(name)).unwrap()); .then(|| writeln!(self.cdef, "{};", T::cdecl(name)).unwrap());
self self
} }
pub fn preload<T: Type>(&mut self, name: impl Display) -> &mut Self {
self.include::<T>();
self
}
pub fn done(&self) -> String { pub fn done(&self) -> String {
self.to_string() self.to_string()
} }

View File

@ -7,6 +7,7 @@ edition = "2024"
proc-macro = true proc-macro = true
[dependencies] [dependencies]
darling = "0.20.11"
proc-macro2 = "1.0.95" proc-macro2 = "1.0.95"
quote = "1.0.40" quote = "1.0.40"
syn = { version = "2.0.103", features = ["full", "visit-mut"] } syn = { version = "2.0.103", features = ["full", "visit-mut"] }

View File

@ -1,9 +1,13 @@
use crate::utils::{ffi_crate, syn_assert, syn_error}; use crate::utils::{ffi_crate, syn_assert, syn_error};
use darling::FromMeta;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{format_ident, quote}; use quote::{format_ident, quote};
use syn::{spanned::*, *}; use syn::{spanned::*, *};
pub fn transform(mut item: Item) -> Result<TokenStream> { #[derive(Debug, FromMeta)]
pub struct Args {}
pub fn transform(_args: Args, mut item: Item) -> Result<TokenStream> {
let (name, impl_type, impl_cdef) = match item { let (name, impl_type, impl_cdef) = match item {
Item::Struct(ref mut str) => ( Item::Struct(ref mut str) => (
str.ident.clone(), str.ident.clone(),

View File

@ -1,3 +1,4 @@
use darling::{FromMeta, ast::NestedMeta};
use proc_macro::TokenStream as TokenStream1; use proc_macro::TokenStream as TokenStream1;
use quote::ToTokens; use quote::ToTokens;
use syn::parse_macro_input; use syn::parse_macro_input;
@ -8,7 +9,9 @@ mod utils;
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn cdef(args: TokenStream1, input: TokenStream1) -> TokenStream1 { pub fn cdef(args: TokenStream1, input: TokenStream1) -> TokenStream1 {
cdef::transform(parse_macro_input!(input)) NestedMeta::parse_meta_list(args.into())
.and_then(|meta| cdef::Args::from_list(&meta).map_err(Into::into))
.and_then(|args| cdef::transform(args, syn::parse(input)?))
.unwrap_or_else(|err| err.into_compile_error().into_token_stream()) .unwrap_or_else(|err| err.into_compile_error().into_token_stream())
.into() .into()
} }

View File

@ -26,25 +26,36 @@ pub fn transform(mut imp: ItemImpl) -> Result<TokenStream> {
} }
fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> { fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
let ty = imp.self_ty.clone();
let ty_name = ty_name(&ty)?;
let ffi = ffi_crate(); let ffi = ffi_crate();
let ffi_funcs = get_ffi_functions(imp)?; let ffi_funcs = get_ffi_functions(imp)?;
let ffi_wrappers: Vec<_> = ffi_funcs let ffi_wrappers: Vec<_> = ffi_funcs
.iter() .iter()
.map(generate_ffi_wrapper) .map(generate_ffi_wrapper)
.collect::<Result<_>>()?; .collect::<Result<_>>()?;
let ffi_register: Vec<_> = ffi_funcs let ffi_register: Vec<_> = ffi_funcs
.iter() .iter()
.map(generate_ffi_register) .map(generate_ffi_register)
.collect::<Result<_>>()?; .collect::<Result<_>>()?;
let ffi_drop_fn = format_ident!("__ffi_drop");
let ffi_drop_name = format!("{ty_name}_drop");
let ffi_exports = {
let mut names = vec![&ffi_drop_fn];
names.extend(ffi_funcs.iter().map(|f| &f.rust_name));
generate_ffi_exports(&ty, names.into_iter())?
};
let lua_funcs = get_lua_functions(imp)?; let lua_funcs = get_lua_functions(imp)?;
let lua_register: Vec<_> = lua_funcs let lua_register: Vec<_> = lua_funcs
.iter() .iter()
.map(generate_lua_register) .map(generate_lua_register)
.collect::<Result<_>>()?; .collect::<Result<_>>()?;
let ty = &*imp.self_ty;
Ok(quote! { Ok(quote! {
unsafe impl #ffi::Metatype for #ty { unsafe impl #ffi::Metatype for #ty {
type Target = Self; type Target = Self;
@ -52,10 +63,22 @@ fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
fn build(b: &mut #ffi::MetatypeBuilder) { fn build(b: &mut #ffi::MetatypeBuilder) {
#(#ffi_register)* #(#ffi_register)*
#(#lua_register)* #(#lua_register)*
b.declare::<unsafe extern "C" fn(*mut Self)>(#ffi_drop_name);
b.metatable_raw("gc", ::std::format_args!("C.{}", #ffi_drop_name));
} }
} }
impl #ty { #(#ffi_wrappers)* } impl #ty {
#(#ffi_wrappers)*
#[unsafe(export_name = #ffi_drop_name)]
unsafe extern "C" fn #ffi_drop_fn(&mut self) {
unsafe { ::std::ptr::drop_in_place(self) }
}
}
#ffi_exports
}) })
} }
@ -213,6 +236,19 @@ fn generate_ffi_register(func: &FfiFunction) -> Result<TokenStream> {
}) })
} }
fn generate_ffi_exports<'a>(
ty: &Type,
names: impl Iterator<Item = &'a Ident>,
) -> Result<TokenStream> {
Ok(quote! {
// hack to prevent ffi functions from being dead code-eliminated
#[used]
static __FFI_EXPORTS: &[fn()] = unsafe {
&[#(::std::mem::transmute(#ty::#names as *const ())),*]
};
})
}
struct LuaFunction { struct LuaFunction {
name: String, name: String,
params: Vec<PatType>, params: Vec<PatType>,

View File

@ -1,3 +1,168 @@
fn main() { use clap::Parser;
println!("Hello, world!"); use lb_core::{GlobalState, PrettyError};
use mimalloc::MiMalloc;
use owo_colors::OwoColorize;
use std::{backtrace::Backtrace, net::SocketAddr, num::NonZero, panic, thread};
use tokio::{runtime, task::LocalSet};
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
fn panic_cb(panic: &panic::PanicHookInfo) {
let trace = Backtrace::force_capture();
let location = panic.location().unwrap();
let payload = panic.payload();
let msg = if let Some(s) = payload.downcast_ref::<&'static str>() {
s
} else if let Some(s) = payload.downcast_ref::<String>() {
s.as_str()
} else {
"unknown error"
};
eprint!(
"{}",
PrettyError::new(msg)
.with_trace(trace)
.prepend(format_args!(
"thread '{}' panicked at {location}",
thread::current().name().unwrap_or("<unnamed>")
))
);
}
#[derive(Debug, Parser)]
struct Args {
/// Paths to scripts to execute.
#[clap(value_name = "SCRIPTS")]
paths: Vec<String>,
/// Strings to execute.
#[clap(long, short = 'e', value_name = "CHUNK")]
evals: Vec<String>,
/// Libraries to require on startup.
#[clap(long, short = 'l', value_name = "NAME")]
libs: Vec<String>,
/// Console log level.
#[clap(long, value_name = "LEVEL", default_value = "debug")]
log_level: tracing::Level,
/// Number of runtime worker threads.
#[clap(long, value_name = "THREADS", default_value_t = Self::threads())]
threads: NonZero<usize>,
/// Number of runtime blocking threads.
#[clap(long, value_name = "THREADS", default_value_t = Self::blocking_threads())]
blocking_threads: NonZero<usize>,
/// Enable tokio-console integration.
#[clap(long)]
enable_console: bool,
/// tokio-console publish address.
#[clap(long, value_name = "ADDRESS", default_value = "127.0.0.1:6669")]
console_addr: SocketAddr,
}
impl Args {
fn threads() -> NonZero<usize> {
thread::available_parallelism().unwrap_or(NonZero::new(1).unwrap())
}
fn blocking_threads() -> NonZero<usize> {
NonZero::new(1024).unwrap()
}
}
fn main() {
panic::set_hook(Box::new(panic_cb));
let args = Args::parse();
init_logger(&args);
let runtime = init_runtime(&args);
GlobalState::set(init_vm(&args));
let main = LocalSet::new();
main.spawn_local(run(args));
runtime.block_on(main);
}
fn init_logger(args: &Args) {
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{Layer, util::*};
let console = tracing_subscriber::fmt()
.compact()
.with_env_filter(
tracing_subscriber::EnvFilter::builder()
.with_default_directive(LevelFilter::from(args.log_level).into())
.from_env_lossy(),
)
.with_file(false)
.with_line_number(false)
.with_target(false)
.finish();
if args.enable_console {
console_subscriber::ConsoleLayer::builder()
.with_default_env()
.server_addr(args.console_addr)
.spawn()
.with_subscriber(console)
.init()
} else {
console.init()
}
}
fn init_runtime(args: &Args) -> runtime::Runtime {
if args.threads.get() == 1 {
runtime::Builder::new_current_thread()
} else {
runtime::Builder::new_multi_thread()
}
.enable_all()
.thread_name("lb")
.worker_threads(args.threads.get() - 1)
.max_blocking_threads(args.blocking_threads.get())
.build()
.unwrap_or_else(|err| panic!("failed to initialise runtime: {err}"))
}
fn init_vm(_args: &Args) -> luajit::State {
let mut state =
luajit::State::new().unwrap_or_else(|err| panic!("failed to initialise runtime: {err}"));
let mut registry = luaffi::Registry::new();
registry.include::<lb_core::lb_core>();
println!("{registry}");
state
.load(Some("@[luby]"), registry.done(), luajit::LoadMode::TEXT)
.and_then(|()| state.call(0, 0))
.unwrap_or_else(|err| panic!("failed to load modules: {err}"));
state
}
async fn run(args: Args) {
let mut state = GlobalState::new_thread();
for ref path in args.paths {
let chunk = match std::fs::read(path) {
Ok(chunk) => chunk,
Err(err) => return eprintln!("{}", format!("{path}: {err}").red()),
};
if let Err(err) = state.load(Some(format!("@{path}")), chunk, Default::default()) {
return eprintln!("{}", err.red());
}
state
.call_async(0)
.await
.unwrap_or_else(GlobalState::uncaught_error);
}
} }

6
test.lua Normal file
View File

@ -0,0 +1,6 @@
local ffi = require("ffi")
local lb = ffi.new("struct lb_core")
print(lb)
lb.spawn("")