Compare commits

..

No commits in common. "e71d618d10c7097c99b243de9540c050ec461eec" and "8f6fc64f7aa643f7d65481d2c30cc8ca2c1e4bfd" have entirely different histories.

45 changed files with 1999 additions and 6158 deletions

View File

@ -1,3 +0,0 @@
[build]
rustflags = ["--cfg", "tokio_unstable"]
rustdocflags = ["--cfg", "tokio_unstable"]

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "luajit"]
path = crates/luajit-sys/src
path = crates/luajit/src
url = https://github.com/LuaJIT/LuaJIT.git

View File

@ -1,5 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"runtime.version": "LuaJIT",
"diagnostics.disable": ["redefined-local", "lowercase-global"]
"diagnostics.disable": ["redefined-local"]
}

1411
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,15 @@
[workspace]
members = [
"crates/lb",
"crates/luaffi",
"crates/luaffi_impl",
"crates/luaify",
"crates/luajit",
"crates/luajit-sys",
]
[profile]
dev.panic = "abort"
release.panic = "abort"
[package]
name = "luby"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.5.40", features = ["derive"] }
console-subscriber = "0.4.1"
lb = { version = "0.1.0", path = "crates/lb" }
luajit = { version = "0.1.0", path = "crates/luajit", features = ["runtime"] }
mimalloc = "0.1.47"
owo-colors = "4.2.1"
sysexits = "0.9.0"
tokio = { version = "1.45.1", features = ["full", "tracing"] }
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
luajit = { version = "0.1.0", path = "crates/luajit" }

View File

@ -1,10 +0,0 @@
[jobs.test]
command = ["cargo", "test", "--workspace"]
need_stdout = true
[jobs.doc]
command = ["cargo", "doc", "--workspace"]
[jobs.doc-open]
command = ["cargo", "doc", "--workspace", "--open"]
on_success = "back"

View File

@ -1,16 +0,0 @@
[package]
name = "lb"
version = "0.1.0"
edition = "2024"
[dependencies]
camino = "1.1.10"
derive_more = { version = "2.0.1", features = ["full"] }
luaffi = { version = "0.1.0", path = "../luaffi" }
luajit = { version = "0.1.0", path = "../luajit" }
sysexits = "0.9.0"
tokio = { version = "1.45.1", features = ["rt", "time", "fs", "net", "process", "signal"] }
[dev-dependencies]
luaify = { path = "../luaify" }
tokio = { version = "1.45.1", features = ["full"] }

View File

@ -1,64 +0,0 @@
// use flume::{Receiver, Sender};
use luaffi::{cdef, metatype};
#[cdef]
pub struct lb_libchannel;
#[metatype]
impl lb_libchannel {
#[new]
extern "Lua-C" fn new() -> Self {
Self
}
extern "Lua" fn unbounded(self) {
let (send, recv) = (__new(__ct.lb_sender), __new(__ct.lb_receiver));
self.__unbounded(send, recv);
(send, recv)
}
extern "Lua" fn bounded(self, cap: number) {
assert(cap >= 0, "channel capacity must be nonnegative");
let (send, recv) = (__new(__ct.lb_sender), __new(__ct.lb_receiver));
self.__bounded(cap, send, recv);
(send, recv)
}
// extern "Lua-C" fn __unbounded(&self, s: *mut lb_sender, r: *mut lb_receiver) {
// let (send, recv) = flume::unbounded();
// unsafe {
// ptr::write(s, lb_sender { send });
// ptr::write(r, lb_receiver { recv });
// }
// }
// extern "Lua-C" fn __bounded(&self, cap: usize, s: *mut lb_sender, r: *mut lb_receiver) {
// let (send, recv) = flume::bounded(cap);
// unsafe {
// ptr::write(s, lb_sender { send });
// ptr::write(r, lb_receiver { recv });
// }
// }
}
// #[cdef]
// pub struct lb_sender {
// #[opaque]
// send: Sender<c_int>,
// }
// #[metatype]
// impl lb_sender {
// extern "Lua" fn send(self, value: _) {
// let key = __ref(value);
// }
// }
// #[cdef]
// pub struct lb_receiver {
// #[opaque]
// recv: Receiver<c_int>,
// }
// #[metatype]
// impl lb_receiver {}

View File

@ -1,32 +0,0 @@
//! The `lb:fs` module provides utilities for interacting with the file system asynchronously.
//!
//! See [`lb_libfs`] for items exported by this module.
use luaffi::{cdef, metatype};
use std::io;
use tokio::fs;
/// Items exported by the `lb:fs` module.
///
/// This module can be obtained by calling `require` in Lua.
///
/// ```lua
/// local fs = require("lb:fs");
/// ```
#[cdef]
pub struct lb_libfs;
#[metatype]
impl lb_libfs {
#[new]
extern "Lua-C" fn new() -> Self {
Self
}
pub extern "Lua" fn read(&self, path: string) -> string {
self.__read(path)
}
async extern "Lua-C" fn __read(&self, path: &str) -> io::Result<Vec<u8>> {
fs::read(path).await
}
}

View File

@ -1,5 +0,0 @@
pub mod channel;
pub mod fs;
pub mod net;
pub mod runtime;
pub mod task;

View File

@ -1,345 +0,0 @@
//! The `lb:net` module provides an asynchronous network API for creating TCP or UDP servers and
//! clients.
//!
//! See [`lb_libnet`] for items exported by this module.
use derive_more::{From, FromStr};
use luaffi::{cdef, metatype};
use std::{
io,
net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
};
use tokio::net::{TcpListener, TcpSocket, TcpStream};
/// Items exported by the `lb:net` module.
///
/// This module can be obtained by calling `require` in Lua.
///
/// ```lua
/// local net = require("lb:net");
/// ```
#[cdef]
pub struct lb_libnet;
#[metatype]
impl lb_libnet {
#[new]
extern "Lua-C" fn new() -> Self {
Self
}
/// See [`Ipv4Addr::LOCALHOST`].
pub extern "Lua-C" fn localhost_v4(&self) -> lb_ipaddr {
lb_ipaddr(Ipv4Addr::LOCALHOST.into())
}
/// See [`Ipv6Addr::LOCALHOST`].
pub extern "Lua-C" fn localhost_v6(&self) -> lb_ipaddr {
lb_ipaddr(Ipv6Addr::LOCALHOST.into())
}
/// See [`Ipv4Addr::UNSPECIFIED`].
pub extern "Lua-C" fn unspecified_v4(&self) -> lb_ipaddr {
lb_ipaddr(Ipv4Addr::UNSPECIFIED.into())
}
/// See [`Ipv6Addr::UNSPECIFIED`].
pub extern "Lua-C" fn unspecified_v6(&self) -> lb_ipaddr {
lb_ipaddr(Ipv6Addr::UNSPECIFIED.into())
}
/// See [`Ipv4Addr::BROADCAST`].
pub extern "Lua-C" fn broadcast_v4(&self) -> lb_ipaddr {
lb_ipaddr(Ipv4Addr::BROADCAST.into())
}
/// Creates an [`lb_ipaddr`] from the given input.
///
/// If `s` is an [`lb_ipaddr`], a copy of that value is returned. If `s` is an
/// [`lb_socketaddr`], the IP address part of the socket address is returned. Otherwise, parses
/// `s` as an IP address string. Both IPv4 or IPv6 addresses are supported.
///
/// # Errors
///
/// Throws if `s` cannot be parsed as an IP address.
pub extern "Lua" fn ipaddr(&self, s: any) -> lb_ipaddr {
if __istype(__ct.lb_ipaddr, s) {
__new(__ct.lb_ipaddr, s) // copy constructor
} else if __istype(__ct.lb_socketaddr, s) {
s.ip()
} else {
self.__parse_ipaddr(s)
}
}
extern "Lua-C" fn __parse_ipaddr(&self, s: &str) -> Result<lb_ipaddr, AddrParseError> {
s.parse()
}
/// Creates an [`lb_socketaddr`] from the given input.
///
/// A socket address is an IP address with a port number.
///
/// If `s` is an [`lb_socketaddr`], a copy of that value is returned. If `s` is an
/// [`lb_ipaddr`], a socket address with that IP address is returned. Otherwise, parses `s` as a
/// socket address string. Both IPv4 and IPv6 addresses are supported.
///
/// If `port` is not specified, `0` is used as the default.
///
/// # Errors
///
/// Throws if `s` cannot be parsed as an IP or socket address.
pub extern "Lua" fn socketaddr(&self, s: any, port: any) -> lb_socketaddr {
if port != () {
self.__new_socketaddr(self.ipaddr(s), port)
} else {
if __istype(__ct.lb_socketaddr, s) {
__new(__ct.lb_socketaddr, s) // copy constructor
} else if __istype(__ct.lb_ipaddr, s) {
self.__new_socketaddr(s, 0) // default port 0
} else {
self.__parse_socketaddr(s)
}
}
}
extern "Lua-C" fn __new_socketaddr(&self, ip: &lb_ipaddr, port: u16) -> lb_socketaddr {
SocketAddr::new(ip.0, port).into()
}
extern "Lua-C" fn __parse_socketaddr(&self, s: &str) -> Result<lb_socketaddr, AddrParseError> {
s.parse()
}
/// Creates a new TCP socket configured for IPv4.
///
/// See [`TcpSocket::new_v4`].
///
/// # Errors
///
/// Throws if an error was encountered during the socket creation.
pub extern "Lua" fn tcp_v4(&self) -> lb_tcpsocket {
self.__new_tcp_v4()
}
/// Creates a new TCP socket configured for IPv6.
///
/// See [`TcpSocket::new_v6`].
///
/// # Errors
///
/// Throws if an error was encountered during the socket creation.
pub extern "Lua" fn tcp_v6(&self) -> lb_tcpsocket {
self.__new_tcp_v6()
}
extern "Lua-C" fn __new_tcp_v4(&self) -> io::Result<lb_tcpsocket> {
TcpSocket::new_v4().map(lb_tcpsocket)
}
extern "Lua-C" fn __new_tcp_v6(&self) -> io::Result<lb_tcpsocket> {
TcpSocket::new_v6().map(lb_tcpsocket)
}
}
/// An IP address, either IPv4 or IPv6.
///
/// # Example
///
/// This example creates an [`lb_ipaddr`] by parsing an IP address string.
///
/// ```lua
/// local net = require("lb:net");
/// local addr = net:ipaddr("127.0.0.1"); -- ipv4 loopback address
///
/// assert(addr:is_v4());
/// assert(addr:is_loopback());
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, From, FromStr)]
#[cdef]
pub struct lb_ipaddr(#[opaque] IpAddr);
#[metatype]
impl lb_ipaddr {
/// See [`IpAddr::is_unspecified`].
pub extern "Lua-C" fn is_unspecified(&self) -> bool {
self.0.is_unspecified()
}
/// See [`IpAddr::is_loopback`].
pub extern "Lua-C" fn is_loopback(&self) -> bool {
self.0.is_loopback()
}
/// See [`IpAddr::is_multicast`].
pub extern "Lua-C" fn is_multicast(&self) -> bool {
self.0.is_multicast()
}
/// Returns the string `"v4"` if this is an IPv4 address or `"v6"` if this is an IPv6 address.
pub extern "Lua" fn family(&self) -> string {
if self.is_v6() { "v6" } else { "v4" }
}
/// Returns `true` if this is an IPv4 address.
pub extern "Lua-C" fn is_v4(&self) -> bool {
self.0.is_ipv4()
}
/// See [`Ipv4Addr::is_private`].
pub extern "Lua-C" fn is_v4_private(&self) -> bool {
match self.0 {
IpAddr::V4(v4) => v4.is_private(),
IpAddr::V6(_) => false,
}
}
/// See [`Ipv4Addr::is_link_local`].
pub extern "Lua-C" fn is_v4_link_local(&self) -> bool {
match self.0 {
IpAddr::V4(v4) => v4.is_link_local(),
IpAddr::V6(_) => false,
}
}
/// See [`Ipv4Addr::is_broadcast`].
pub extern "Lua-C" fn is_v4_broadcast(&self) -> bool {
match self.0 {
IpAddr::V4(v4) => v4.is_broadcast(),
IpAddr::V6(_) => false,
}
}
/// See [`Ipv4Addr::is_documentation`].
pub extern "Lua-C" fn is_v4_documentation(&self) -> bool {
match self.0 {
IpAddr::V4(v4) => v4.is_documentation(),
IpAddr::V6(_) => false,
}
}
/// Returns `true` if this is an IPv6 address.
pub extern "Lua-C" fn is_v6(&self) -> bool {
self.0.is_ipv6()
}
/// See [`Ipv6Addr::is_unique_local`].
pub extern "Lua-C" fn is_v6_unique_local(&self) -> bool {
match self.0 {
IpAddr::V4(_) => false,
IpAddr::V6(v6) => v6.is_unique_local(),
}
}
/// See [`Ipv6Addr::is_unicast_link_local`].
pub extern "Lua-C" fn is_v6_unicast_link_local(&self) -> bool {
match self.0 {
IpAddr::V4(_) => false,
IpAddr::V6(v6) => v6.is_unicast_link_local(),
}
}
/// See [`Ipv4Addr::to_ipv6_compatible`].
pub extern "Lua-C" fn to_v6_compat(&self) -> Self {
match self.0 {
IpAddr::V4(v4) => Self(v4.to_ipv6_compatible().into()),
IpAddr::V6(_) => *self,
}
}
/// See [`Ipv4Addr::to_ipv6_mapped`].
pub extern "Lua-C" fn to_v6_mapped(&self) -> Self {
match self.0 {
IpAddr::V4(v4) => Self(v4.to_ipv6_mapped().into()),
IpAddr::V6(_) => *self,
}
}
/// See [`IpAddr::to_canonical`].
pub extern "Lua-C" fn canonical(&self) -> Self {
self.0.to_canonical().into()
}
/// Returns the string representation of this address.
#[tostring]
pub extern "Lua-C" fn tostring(&self) -> String {
self.0.to_string()
}
}
/// A socket address, which is an IP address with a port number.
#[derive(Debug, Clone, Copy, PartialEq, Eq, From, FromStr)]
#[cdef]
pub struct lb_socketaddr(#[opaque] SocketAddr);
#[metatype]
impl lb_socketaddr {
/// Returns the IP part of this address.
pub extern "Lua-C" fn ip(&self) -> lb_ipaddr {
self.0.ip().into()
}
/// Sets the IP part of this address.
///
/// This function accepts the same arguments as [`ipaddr`](lb_libnet::ipaddr).
pub extern "Lua" fn set_ip(&mut self, s: any) -> &mut Self {
if __istype(__ct.lb_ipaddr, s) {
self.__set_ip(s);
} else if __istype(__ct.lb_socketaddr, s) {
self.__set_ip(s.ip());
} else {
self.__set_ip_parse(s);
}
self
}
extern "Lua-C" fn __set_ip(&mut self, ip: &lb_ipaddr) {
self.0.set_ip(ip.0);
}
extern "Lua-C" fn __set_ip_parse(&mut self, s: &str) -> Result<(), AddrParseError> {
s.parse().map(|ip| self.0.set_ip(ip))
}
/// Returns the port part of this address.
pub extern "Lua-C" fn port(&self) -> u16 {
self.0.port()
}
/// Sets the port part of this address.
pub extern "Lua" fn set_port(&mut self, port: number) -> &mut Self {
self.__set_port(port);
self
}
extern "Lua-C" fn __set_port(&mut self, port: u16) {
self.0.set_port(port)
}
/// Returns the string representation of this address.
#[tostring]
pub extern "Lua-C" fn tostring(&self) -> String {
self.0.to_string()
}
}
/// A TCP socket which has not yet been converted to a [`lb_tcpstream`] or [`lb_tcplistener`].
#[derive(Debug, From)]
#[cdef]
pub struct lb_tcpsocket(#[opaque] TcpSocket);
#[metatype]
impl lb_tcpsocket {}
#[derive(Debug, From)]
#[cdef]
pub struct lb_tcpstream(#[opaque] TcpStream);
#[metatype]
impl lb_tcpstream {}
#[derive(Debug, From)]
#[cdef]
pub struct lb_tcplistener(#[opaque] TcpListener);
#[metatype]
impl lb_tcplistener {}

View File

@ -1,5 +0,0 @@
local task = require("lb:task")
function spawn(f, ...)
return task:spawn(f, ...)
end

View File

@ -1,85 +0,0 @@
use crate::{channel::lb_libchannel, fs::lb_libfs, net::lb_libnet, task::lb_libtask};
use derive_more::{Deref, DerefMut};
use luaffi::{Registry, Type};
use luajit::{Chunk, State};
use std::fmt::Display;
use tokio::{
task::{JoinHandle, LocalSet, futures::TaskLocalFuture, spawn_local},
task_local,
};
#[derive(Debug, Default)]
pub struct Builder {
registry: Registry,
}
impl Builder {
pub fn new() -> Self {
let mut registry = Registry::new();
registry
.preload::<lb_libtask>("lb:task")
.preload::<lb_libchannel>("lb:channel")
.preload::<lb_libfs>("lb:fs")
.preload::<lb_libnet>("lb:net");
Self { registry }
}
pub fn module<T: Type>(&mut self, name: impl Display) -> &mut Self {
self.registry.preload::<T>(name);
self
}
pub fn registry(&self) -> &Registry {
&self.registry
}
pub fn build(&self) -> luajit::Result<Runtime> {
Ok(Runtime {
state: {
let mut s = State::new()?;
let mut chunk = Chunk::new(self.registry.done());
chunk.extend(include_bytes!("./runtime.lua"));
s.eval(chunk.path("[luby]"), 0, 0)?;
s
},
tasks: LocalSet::new(),
})
}
}
#[derive(Debug, Deref, DerefMut)]
pub struct Runtime {
#[deref]
#[deref_mut]
state: State,
tasks: LocalSet,
}
task_local! {
static STATE: State;
}
impl Runtime {
pub fn spawn<T: 'static>(
&self,
f: impl AsyncFnOnce(&mut State) -> T + 'static,
) -> JoinHandle<T> {
self.tasks
.spawn_local(async move { f(&mut STATE.with(|s| s.new_thread())).await })
}
}
pub fn spawn<T: 'static>(f: impl AsyncFnOnce(&mut State) -> T + 'static) -> JoinHandle<T> {
spawn_local(async move { f(&mut STATE.with(|s| s.new_thread())).await })
}
impl IntoFuture for Runtime {
type Output = ();
type IntoFuture = TaskLocalFuture<State, LocalSet>;
fn into_future(self) -> Self::IntoFuture {
STATE.scope(self.state, self.tasks)
}
}

View File

@ -1,46 +0,0 @@
use crate::runtime::spawn;
use luaffi::{cdef, metatype};
use std::{ffi::c_int, process};
use tokio::task::JoinHandle;
#[cdef]
pub struct lb_libtask;
#[metatype]
impl lb_libtask {
#[new]
extern "Lua-C" fn new() -> Self {
Self
}
pub extern "Lua" fn spawn(self, f: function, ...) {
// pack the function and its arguments into a table and pass its ref to rust
self.__spawn(__ref(__tpack(f, variadic!())))
}
extern "Lua-C" fn __spawn(&self, key: c_int) -> lb_task {
let handle = spawn(async move |s| {
// SAFETY: key is always unique, created by __ref above
let arg = unsafe { s.new_ref_unchecked(key) };
s.resize(0);
s.push(arg);
let narg = s.unpack(1, 1, None) - 1;
println!("{s:?}");
if let Err(_err) = s.call_async(narg, 0).await {
process::exit(1)
}
println!("{s:?}");
});
lb_task { handle }
}
}
#[cdef]
pub struct lb_task {
#[opaque]
handle: JoinHandle<()>,
}
#[metatype]
impl lb_task {}

View File

@ -1,32 +0,0 @@
use lb::runtime;
use luaify::luaify;
use luajit::{Chunk, LoadMode};
use tokio::test;
async fn run_lua(s: &'static str) {
let rt = runtime::Builder::new().build().unwrap();
let task = rt.spawn(async move |state| {
println!("executing test chunk: {s}");
state
.load(Chunk::new(s).mode(LoadMode::TEXT))
.unwrap_or_else(|err| panic!("{err}"));
state
.call_async(0, 0)
.await
.unwrap_or_else(|err| panic!("{err}"));
});
rt.await;
task.await.unwrap_or_else(|err| panic!("{err}"));
}
#[test]
async fn ipaddr() {
run_lua(luaify!({
let net = require("lb:net");
print(net.ipaddr("127.0.0.1"));
}))
.await
}

View File

@ -1,35 +0,0 @@
use lb::runtime;
use luaify::luaify;
use luajit::{Chunk, LoadMode};
use tokio::test;
async fn run_lua(s: &'static str) {
let rt = runtime::Builder::new().build().unwrap();
let task = rt.spawn(async move |state| {
println!("executing test chunk: {s}");
state
.load(Chunk::new(s).mode(LoadMode::TEXT))
.unwrap_or_else(|err| panic!("{err}"));
state
.call_async(0, 0)
.await
.unwrap_or_else(|err| panic!("{err}"));
});
rt.await;
task.await.unwrap_or_else(|err| panic!("{err}"));
}
#[test]
async fn task_test() {
run_lua(luaify!({
let thing = spawn(|| {
print("spawn callback!!!!!!!!!!!!!");
});
print("thing is", thing);
//
}))
.await
}

View File

@ -9,3 +9,4 @@ luaffi_impl = { version = "0.1.0", path = "../luaffi_impl" }
luaify = { version = "0.1.0", path = "../luaify" }
rustc-hash = "2.1.1"
simdutf8 = "0.1.5"
static_assertions = "1.1.0"

View File

@ -1,49 +1,32 @@
use crate::{
__internal::{display, type_id},
Cdef, CdefBuilder, FfiReturnConvention, IntoFfi, Metatype, MetatypeBuilder, Type, TypeBuilder,
TypeType, UnsafeExternCFn,
CDef, CDefBuilder, Metatype, MetatypeBuilder, ToFfi, Type, TypeBuilder,
};
use luaify::luaify;
use std::{
fmt::Display,
marker::PhantomPinned,
mem,
pin::Pin,
ptr,
task::{Context, Poll},
};
// guardrail bytes prepended to all lua_future values in the header part to hopefully try to prevent
// misinterpreting other kinds of cdata as a lua_future if the lua user code yields a cdata that is
// not a lua_future for some odd reason.
type Signature = u64;
const SIGNATURE: Signature = Signature::from_ne_bytes(*b"\x00lb_poll");
#[repr(C)]
#[allow(non_camel_case_types)]
pub struct lua_future<F: Future<Output: IntoFfi>> {
pub struct lua_future<F: Future<Output: ToFfi>> {
//
// SAFETY: LuaJIT guarantees that cdata payloads, which are GC-managed, are never relocated
// (i.e. pinned). We can safely assume that we are pinned and poll the future inside this
// wrapper. We use this to our advantage by storing the future value directly inside the cdata
// payload instead of boxing the future and introducing indirection.
//
// https://github.com/LuaJIT/LuaJIT/issues/1167#issuecomment-1968047229
//
// .sig and .poll fields MUST come first. .poll is only to be called by the Rust async runtime
// to advance the future without knowing its type (see `lua_pollable` below).
// SAFETY: .poll MUST be the first field. It is only to be called by the Rust async runtime to
// advance the future without knowing its type (see `lua_pollable` below).
//
// This assumes that the crate containing the async runtime and the crate containing the future
// type are ABI-compatible (compiled by the same compiler with the same target into the same
// binary). This is always the case for luby because all modules are statically linked into one
// binary.
// type are ABI-compatible (compiled by the same compiler with the same target into the same binary).
// This is always the case for luby because all modules are statically linked into one binary.
//
// only .take and .drop are visible to Lua itself; other fields are opaque.
// .poll and .state are opaque to Lua itself.
//
sig: Signature,
poll: fn(Pin<&mut Self>, cx: &mut Context) -> Poll<()>,
poll: fn(&mut Self, cx: &mut Context) -> Poll<()>,
state: State<F>,
take: unsafe extern "C" fn(&mut Self) -> <F::Output as IntoFfi>::Into,
take: unsafe extern "C" fn(&mut Self) -> <F::Output as ToFfi>::To,
drop: unsafe extern "C" fn(&mut Self),
}
@ -51,17 +34,15 @@ pub struct lua_future<F: Future<Output: IntoFfi>> {
#[allow(non_camel_case_types)]
pub struct lua_pollable {
//
// SAFETY: The only way to obtain a reference to a `lua_pollable` is by returning a
// `lua_future<T>` from Rust to Lua, which LuaJIT boxes into cdata and `coroutine.yield`'s back
// to Rust, then casting the yielded pointer value to `*mut lua_pollable`.
// SAFETY: The only way to obtain a reference to a `lua_pollable` is by returning a `lua_future<T>`
// from Rust to Lua, which LuaJIT boxes into cdata and `coroutine.yield`'s back to Rust, then
// casting the yielded pointer value to `*mut lua_pollable`.
//
// This is the type-erased "header" part of a `lua_future<T>` which allows the async runtime to
// poll the future without knowing its concrete type (essentially dynamic dispatch). It has the
// same layout as `lua_future<T>` without the state part.
//
sig: Signature,
poll: fn(Pin<&mut Self>, cx: &mut Context) -> Poll<()>,
_phantom: PhantomPinned,
poll: fn(&mut Self, cx: &mut Context) -> Poll<()>,
}
enum State<F: Future> {
@ -70,10 +51,9 @@ enum State<F: Future> {
Complete,
}
impl<F: Future<Output: IntoFfi>> lua_future<F> {
impl<F: Future<Output: ToFfi>> lua_future<F> {
pub fn new(fut: F) -> Self {
Self {
sig: SIGNATURE,
poll: Self::poll,
state: State::Pending(fut),
take: Self::take,
@ -81,29 +61,39 @@ impl<F: Future<Output: IntoFfi>> lua_future<F> {
}
}
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {
// SAFETY: we do not ever move the future here, so this is safe
let this = unsafe { Pin::into_inner_unchecked(self) };
match this.state {
fn poll(&mut self, cx: &mut Context) -> Poll<()> {
//
// SAFETY: LuaJIT guarantees that cdata payloads, which are GC-managed, are never
// relocated (i.e. pinned). We can safely assume that we are pinned and poll the future.
//
// We use this to our advantage by storing the future value directly inside the cdata
// payload instead of boxing the future and introducing indirection.
//
// https://github.com/LuaJIT/LuaJIT/issues/1167#issuecomment-1968047229
//
match self.state {
State::Pending(ref mut fut) => match unsafe { Pin::new_unchecked(fut) }.poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(value) => Poll::Ready(this.state = State::Fulfilled(value)), // drop the future in-place
Poll::Ready(value) => Poll::Ready(self.state = State::Fulfilled(value)),
},
State::Fulfilled(_) => Poll::Ready(()),
State::Complete => unreachable!("lua_future::poll() called on a completed future"),
State::Complete => unreachable!("lua_future::poll() called on completed future"),
}
}
unsafe extern "C" fn take(&mut self) -> <F::Output as IntoFfi>::Into {
// `fut:__take()` returns the fulfilled value by-value (not by out-param) because if we
// preallocate a cdata for the out-param and the thread for some reason gets dropped and
// never resumed, the GC could call the destructor on an uninitialised cdata.
unsafe extern "C" fn take(&mut self) -> <F::Output as ToFfi>::To {
// `fut:__take()` returns the fulfilled value by-value because it is the lowest common
// denominator for supported return conventions (all `ToFfi` impls support return by-value;
// primitives e.g. don't support return by out-param because they get boxed in cdata).
//
// Plus, if we preallocate a cdata for out-param and the thread for some reason gets dropped
// and never resumed, GC could call the destructor on an uninitialised cdata.
match self.state {
State::Fulfilled(_) => match mem::replace(&mut self.state, State::Complete) {
State::Fulfilled(value) => value.convert(),
_ => unreachable!(),
},
State::Pending(_) => panic!("lua_future::take() called on a pending future"),
State::Pending(_) => panic!("lua_future::take() called on pending future"),
State::Complete => panic!("lua_future::take() called twice"),
}
}
@ -113,35 +103,22 @@ impl<F: Future<Output: IntoFfi>> lua_future<F> {
}
}
impl lua_pollable {
pub fn is_valid(&self) -> bool {
// TODO: signature check can currently read out-of-bounds if lua code for some reason yields
// a cdata of size less than 8 bytes that is not a lua_future. there is no easy way to fix
// afaik this because there is no way to find the size of a cdata payload using the C API.
self.sig == SIGNATURE
}
}
impl Future for lua_pollable {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
assert!(self.is_valid(), "invalid lua_pollable value");
(self.poll)(self, cx)
// SAFETY: see comment above in `lua_future::poll()`
(self.poll)(Pin::into_inner(self), cx)
}
}
unsafe impl<F: Future<Output: IntoFfi> + 'static> Type for lua_future<F> {
unsafe impl<F: Future<Output: ToFfi> + 'static> Type for lua_future<F> {
fn name() -> impl Display {
display!("future__{:x}", type_id::<F>())
}
fn ty() -> TypeType {
TypeType::Aggregate
}
fn cdecl(name: impl Display) -> impl Display {
display!("struct {} {name}", Self::name())
display!("struct future__{:x} {name}", type_id::<F>())
}
fn build(s: &mut TypeBuilder) {
@ -149,15 +126,15 @@ unsafe impl<F: Future<Output: IntoFfi> + 'static> Type for lua_future<F> {
}
}
unsafe impl<F: Future<Output: IntoFfi> + 'static> Cdef for lua_future<F> {
fn build(s: &mut CdefBuilder) {
s.field_opaque(mem::offset_of!(Self, take)) // opaque .sig, .poll and .state
.field::<UnsafeExternCFn<(&mut Self,), <F::Output as IntoFfi>::Into>>("__take")
.field::<UnsafeExternCFn<(&mut Self,), ()>>("__drop");
unsafe impl<F: Future<Output: ToFfi> + 'static> CDef for lua_future<F> {
fn build(s: &mut CDefBuilder) {
s.field_opaque(mem::offset_of!(Self, take))
.field::<unsafe extern "C" fn(*mut Self) -> <F::Output as ToFfi>::To>("__take")
.field::<unsafe extern "C" fn(*mut Self)>("__drop");
}
}
unsafe impl<F: Future<Output: IntoFfi> + 'static> Metatype for lua_future<F> {
unsafe impl<F: Future<Output: ToFfi> + 'static> Metatype for lua_future<F> {
type Target = Self;
fn build(s: &mut MetatypeBuilder) {
@ -165,15 +142,10 @@ unsafe impl<F: Future<Output: IntoFfi> + 'static> Metatype for lua_future<F> {
}
}
unsafe impl<F: Future<Output: IntoFfi> + 'static> IntoFfi for lua_future<F> {
type Into = lua_future<F>;
unsafe impl<F: Future<Output: ToFfi> + 'static> ToFfi for lua_future<F> {
type To = lua_future<F>;
fn convention() -> FfiReturnConvention {
// futures are always returned by-value due to rust type inference limitations
FfiReturnConvention::ByValue
}
fn convert(self) -> Self::Into {
fn convert(self) -> Self::To {
self
}
@ -183,18 +155,17 @@ unsafe impl<F: Future<Output: IntoFfi> + 'static> IntoFfi for lua_future<F> {
// gets resumed. Lua user code should never to worry about awaiting futures.
//
// Once the current thread gets resumed and we take the future's fulfilled value, we clear
// the finaliser on the future and forget it (there is nothing to drop once the value is
// taken).
// the finaliser on the future and forget it (there is nothing to call drop on).
//
// `coroutine.yield` is cached as `__yield` and `ffi.gc` as `__gc` in locals (see lib.rs)
// `coroutine.yield` is cached as `yield` and `ffi.gc` as `gc` in locals (see lib.rs)
display!(
"__yield({ret}); {ret} = __gc({ret}, nil):__take(); {}",
<F::Output as IntoFfi>::postlude(ret)
"yield({ret}); {ret} = gc({ret}, nil):__take(); {}",
<F::Output as ToFfi>::postlude(ret)
)
}
}
impl<F: IntoFuture<Output: IntoFfi>> From<F> for lua_future<F::IntoFuture> {
impl<F: IntoFuture<Output: ToFfi>> From<F> for lua_future<F::IntoFuture> {
fn from(value: F) -> Self {
Self::new(value.into_future())
}

View File

@ -1,50 +1,23 @@
pub use luaify::*;
use rustc_hash::FxHasher;
pub use static_assertions::*;
use std::{
any::TypeId,
fmt::{self, Display, Formatter},
hash::{Hash, Hasher},
};
#[allow(non_camel_case_types)]
pub mod stub_types {
pub struct any;
pub struct nil;
pub struct boolean;
pub struct lightuserdata;
pub struct number;
pub struct integer;
pub struct string;
pub struct table;
pub struct function;
pub struct userdata;
pub struct thread;
pub struct cdata;
pub struct variadic;
}
pub fn type_id<T: 'static>() -> u64 {
let mut hash = FxHasher::default();
TypeId::of::<T>().hash(&mut hash);
hash.finish()
}
macro_rules! export {
($($fn:expr),+ $(,)?) => {
// this ensures ffi function symbol exports are actually present in the resulting binary,
// otherwise they may get dead code-eliminated before it reaches the linker
#[used]
static __FFI_EXPORTS: &[fn()] = unsafe {
&[$(::std::mem::transmute($fn as *const ())),*]
};
};
}
macro_rules! display {
($($fmt:expr),+) => {{ crate::__internal::disp(move |f| write!(f, $($fmt),+)) }};
}
pub(crate) use {display, export};
pub(crate) use display;
pub fn disp(f: impl Fn(&mut Formatter) -> fmt::Result) -> impl Display {
struct Disp<F: Fn(&mut Formatter) -> fmt::Result>(F);

View File

@ -1,24 +0,0 @@
local LUA_REFNIL = -1 -- lib_aux.c
local FREELIST_REF = 0
local function __ref(value, t)
if value == nil then return LUA_REFNIL end
if t == nil then t = __registry end
local ref = t[FREELIST_REF]
if ref ~= nil and ref ~= 0 then
t[FREELIST_REF] = t[ref]
else
ref = #t + 1
end
t[ref] = value
return ref
end
local function __unref(ref, t)
if ref < 0 then return nil end
if t == nil then t = __registry end
local value = t[ref]
t[ref] = t[FREELIST_REF]
t[FREELIST_REF] = ref
return value
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,80 @@
use crate::{CDef, CDefBuilder, FromFfi, ToFfi, Type, TypeBuilder, display};
use std::{ffi::c_int, fmt::Display, ptr};
#[repr(C)]
#[allow(non_camel_case_types)]
pub enum lua_option<T> {
None, // __tag = 0
Some(T), // __tag = 1
}
unsafe impl<T: Type> Type for lua_option<T> {
fn name() -> impl Display {
display!("option__{}", T::name())
}
fn cdecl(name: impl Display) -> impl Display {
display!("struct option__{} {name}", T::name())
}
fn build(b: &mut TypeBuilder) {
b.include::<T>().cdef::<Self>();
}
}
unsafe impl<T: Type> CDef for lua_option<T> {
fn build(b: &mut CDefBuilder) {
b.field::<c_int>("__tag").field::<T>("__value");
}
}
unsafe impl<T: FromFfi> FromFfi for Option<T> {
type From = *mut Self::FromValue; // pass by-ref
type FromValue = lua_option<T::FromValue>;
const ARG_KEEPALIVE: bool = T::ARG_KEEPALIVE;
fn prelude(arg: &str) -> impl Display {
let ct = Self::FromValue::name();
display!(
"if {arg} == nil then {arg} = {ct}(); else {}{arg} = {ct}(1, {arg}); end; ",
T::prelude(arg)
)
}
fn convert(from: Self::From) -> Self {
debug_assert!(
!from.is_null(),
"Option<T>::convert() called on a null lua_option<T>"
);
Self::convert_value(unsafe { ptr::replace(from, lua_option::None) })
}
fn convert_value(from: Self::FromValue) -> Self {
match from {
lua_option::Some(value) => Some(T::convert_value(value)),
lua_option::None => None,
}
}
}
unsafe impl<T: ToFfi> ToFfi for Option<T> {
type To = lua_option<T::To>;
fn convert(self) -> Self::To {
match self {
Some(value) => lua_option::Some(value.convert()),
None => lua_option::None,
}
}
fn postlude(ret: &str) -> impl Display {
// if we don't have a value, return nil. otherwise copy out the inner value immediately,
// forget the option cdata, then call postlude on the inner value.
display!(
"if {ret}.__tag == 0 then {ret} = nil; else {ret} = {ret}.__value; {}end; ",
T::postlude(ret)
)
}
}

View File

@ -1,72 +0,0 @@
use crate::{
__internal::{disp, display},
Cdef, CdefBuilder, IntoFfi, Type, TypeBuilder, TypeType,
string::{DROP_BUFFER_FN, lua_buffer},
};
use std::{ffi::c_int, fmt::Display};
#[repr(C)]
#[allow(non_camel_case_types)]
pub enum lua_result<T> {
Err(lua_buffer), // __tag = 0
Ok(T), // __tag = 1
}
unsafe impl<T: Type> Type for lua_result<T> {
fn name() -> impl Display {
display!("result__{}", T::name())
}
fn ty() -> TypeType {
TypeType::Aggregate
}
fn cdecl(name: impl Display) -> impl Display {
display!("struct {} {name}", Self::name())
}
fn build(b: &mut TypeBuilder) {
b.cdef::<Self>();
}
}
unsafe impl<T: Type> Cdef for lua_result<T> {
fn build(b: &mut CdefBuilder) {
b.field::<c_int>("__tag").inner_union(|b| {
(T::ty() != TypeType::Void).then(|| b.field::<T>("__value"));
b.field::<lua_buffer>("__err");
});
}
}
unsafe impl<T: IntoFfi, E: Display> IntoFfi for Result<T, E> {
type Into = lua_result<T::Into>;
fn convert(self) -> Self::Into {
match self {
Ok(value) => lua_result::Ok(T::convert(value)),
Err(err) => lua_result::Err(lua_buffer::new(err.to_string())),
}
}
fn postlude(ret: &str) -> impl Display {
disp(move |f| {
let ct = T::Into::name();
write!(f, "if {ret}.__tag ~= 0 then ")?;
match T::Into::ty() {
TypeType::Void => write!(f, "{ret} = nil; "), // for void results, we don't have a __value
TypeType::Primitive => write!(f, "{ret} = {ret}.__value; "),
TypeType::Aggregate => write!(f, "{ret} = __new(__ct.{ct}, {ret}.__value); "),
}?;
write!(f, "{}", T::postlude(ret))?;
write!(
f,
"else \
local __{ret}_msg = __intern({ret}.__err.__ptr, {ret}.__err.__len); \
__C.{DROP_BUFFER_FN}({ret}.__err); \
return error(__{ret}_msg); \
end; "
)
})
}
}

View File

@ -1,271 +1,89 @@
use crate::{
__internal::{disp, display, export},
FromFfi, IntoFfi,
};
use bstr::{BStr, BString};
use crate::{__internal::disp, FromFfi, IS_UTF8_FN, Type};
use luaffi_impl::{cdef, metatype};
use std::{fmt::Display, mem::ManuallyDrop, ptr, slice};
use std::{fmt, ptr, slice};
pub(crate) const IS_UTF8_FN: &str = "luaffi_is_utf8";
pub(crate) const DROP_BUFFER_FN: &str = "luaffi_drop_buffer";
#[unsafe(export_name = "luaffi_is_utf8")]
unsafe extern "C" fn __is_utf8(ptr: *const u8, len: usize) -> bool {
debug_assert!(!ptr.is_null());
simdutf8::basic::from_utf8(unsafe { slice::from_raw_parts(ptr, len) }).is_ok()
}
#[unsafe(export_name = "luaffi_drop_buffer")]
unsafe extern "C" fn __drop_buffer(buf: *mut lua_buffer) {
debug_assert!(!buf.is_null());
debug_assert!(!unsafe { (*buf).__ptr.is_null() });
drop(unsafe { Vec::from_raw_parts((*buf).__ptr, (*buf).__len, (*buf).__cap) })
}
export![__is_utf8, __drop_buffer];
#[derive(Debug, Clone, Copy)]
#[cdef]
#[derive(Debug, Clone, Copy)]
pub struct lua_buf {
__ptr: *const u8,
__len: usize,
}
#[metatype]
impl lua_buf {
// this takes a slice and decomposes it into its raw parts. caller should ensure the result is
// used only as long as the original buffer is still alive.
pub(crate) fn new(s: &[u8]) -> Self {
Self {
__ptr: s.as_ptr(),
__len: s.len(),
}
}
pub(crate) fn null() -> Self {
Self {
__ptr: ptr::null(),
__len: 0,
}
}
}
#[derive(Debug, Clone, Copy)]
#[cdef]
pub struct lua_buffer {
__ptr: *mut u8,
__len: usize,
__cap: usize,
}
#[metatype]
impl lua_buffer {
// this takes ownership of the Vec and decomposes it into its raw parts. the result must be
// dropped by `__drop_buffer` (see [`DROP_BUFFER_FN`]).
pub(crate) fn new(s: impl Into<Vec<u8>>) -> Self {
let s = s.into();
Self {
__cap: s.capacity(),
__len: s.len(),
__ptr: ManuallyDrop::new(s).as_mut_ptr(),
}
}
impl lua_buf {}
pub(crate) fn null() -> Self {
Self {
__ptr: ptr::null_mut(),
__len: 0,
__cap: 0,
}
}
}
unsafe impl FromFfi for *const [u8] {
type From = *const Self::FromValue; // pass by-ref
type FromValue = lua_buf;
unsafe impl<'s> FromFfi for &'s [u8] {
type From = Option<&'s lua_buf>;
const ARG_KEEPALIVE: bool = true;
fn require_keepalive() -> bool {
true
}
fn prelude(arg: &str) -> impl Display {
fn prelude(arg: &str) -> impl fmt::Display {
// this converts string arguments to a `lua_buf` with a pointer to the string and its length
disp(move |f| {
let ct = lua_buf::name();
write!(
f,
r#"assert(type({arg}) == "string", "string expected in argument '{arg}', got " .. type({arg})); "#
r#"if {arg} ~= nil then assert(type({arg}) == "string", "string expected in argument '{arg}', got " .. type({arg})); "#
)?;
// SAFETY: the lua_buf is only valid for as long as the string is alive. we've ensured
// that it is alive for at least the duration of the ffi call via `require_keepalive()`.
write!(f, "{arg} = __new(__ct.lua_buf, {arg}, #{arg}); end; ")
write!(f, "{arg} = {ct}({arg}, #{arg}); end; ")
})
}
fn convert(from: Self::From) -> Self {
// SAFETY: we already checked that the string is nonnull from the lua side
debug_assert!(from.is_some());
let from = unsafe { from.unwrap_unchecked() };
debug_assert!(!from.__ptr.is_null());
unsafe { slice::from_raw_parts(from.__ptr, from.__len) }
if from.is_null() {
ptr::slice_from_raw_parts(ptr::null(), 0)
} else {
// SAFETY: this is safe because lua_buf is copyable
unsafe { Self::convert_value(*from) }
}
}
unsafe impl<'s> FromFfi for &'s str {
type From = Option<&'s lua_buf>;
fn require_keepalive() -> bool {
true
fn convert_value(from: Self::FromValue) -> Self {
ptr::slice_from_raw_parts(from.__ptr, from.__len)
}
}
fn prelude(arg: &str) -> impl Display {
// this converts string arguments to a `lua_buf` with a pointer to the string and its length
// and ensures that the string is valid utf8
unsafe impl FromFfi for &str {
type From = *const Self::FromValue; // pass by-ref
type FromValue = lua_buf;
const ARG_KEEPALIVE: bool = true;
fn prelude(arg: &str) -> impl fmt::Display {
disp(move |f| {
let ct = lua_buf::name();
write!(
f,
r#"assert(type({arg}) == "string", "string expected in argument '{arg}', got " .. type({arg})); "#
)?;
write!(
f,
r#"assert(__C.{IS_UTF8_FN}({arg}, #{arg}), "argument '{arg}' must be a valid utf-8 string"); "#
r#"assert(C.{IS_UTF8_FN}({arg}, #{arg}), "argument '{arg}' must be a valid utf8 string"); "#
)?;
write!(f, "{arg} = __new(__ct.lua_buf, {arg}, #{arg}); ")
write!(f, "{arg} = {ct}({arg}, #{arg}); ")
})
}
fn convert(from: Self::From) -> Self {
// SAFETY: we already checked that the string is nonnull and valid utf8 from the lua side
debug_assert!(from.is_some());
let from = unsafe { from.unwrap_unchecked() };
debug_assert!(!from.__ptr.is_null());
let from = unsafe { slice::from_raw_parts(from.__ptr, from.__len) };
debug_assert!(
std::str::from_utf8(from).is_ok(),
!from.is_null(),
"<&str>::convert() called on a null lua_buf"
);
// SAFETY: this is safe because lua_buf is copyable
unsafe { Self::convert_value(*from) }
}
fn convert_value(from: Self::FromValue) -> Self {
let s = unsafe { slice::from_raw_parts(from.__ptr, from.__len) };
debug_assert!(
std::str::from_utf8(s).is_ok(),
"<&str>::convert() called on an invalid utf8 string when it was checked to be valid"
);
unsafe { std::str::from_utf8_unchecked(from) }
}
}
unsafe impl IntoFfi for &'static [u8] {
type Into = lua_buf;
fn convert(self) -> Self::Into {
// SAFETY: the slice is 'static so the resulting lua_buf is always valid
lua_buf::new(self)
}
fn postlude(ret: &str) -> impl Display {
display!("{ret} = __intern({ret}.__ptr, {ret}.__len)")
// SAFETY: we already checked that the string is valid utf8 from the lua side
unsafe { std::str::from_utf8_unchecked(s) }
}
}
unsafe impl IntoFfi for Vec<u8> {
type Into = lua_buffer;
fn convert(self) -> Self::Into {
lua_buffer::new(self)
}
fn postlude(ret: &str) -> impl Display {
display!(
"do local __{ret} = {ret}; {ret} = __intern({ret}.__ptr, {ret}.__len); __C.{DROP_BUFFER_FN}(__{ret}); end; "
)
}
}
macro_rules! impl_from_via {
($ty:ty, $via:ty) => {
unsafe impl<'s> FromFfi for $ty {
type From = <$via as FromFfi>::From;
fn require_keepalive() -> bool {
<$via as FromFfi>::require_keepalive()
}
fn prelude(arg: &str) -> impl Display {
<$via as FromFfi>::prelude(arg)
}
fn convert(from: Self::From) -> Self {
<$via as FromFfi>::convert(from).into()
}
}
};
}
impl_from_via!(&'s BStr, &'s [u8]);
macro_rules! impl_into_via {
($ty:ty, $via:ty) => {
unsafe impl IntoFfi for $ty {
type Into = <$via as IntoFfi>::Into;
fn convert(self) -> Self::Into {
<$via as IntoFfi>::convert(self.into())
}
fn postlude(ret: &str) -> impl Display {
<$via as IntoFfi>::postlude(ret)
}
}
};
}
impl_into_via!(&'static BStr, &'static [u8]);
impl_into_via!(&'static str, &'static BStr);
impl_into_via!(BString, Vec<u8>);
impl_into_via!(String, BString);
macro_rules! impl_optional_from {
($ty:ty) => {
unsafe impl<'s> FromFfi for Option<$ty> {
type From = <$ty as FromFfi>::From;
fn require_keepalive() -> bool {
<$ty as FromFfi>::require_keepalive()
}
fn prelude(arg: &str) -> impl Display {
// just pass a null pointer if argument is nil
display!(
"if {arg} ~= nil then {}end; ",
<$ty as FromFfi>::prelude(arg)
)
}
fn convert(from: Self::From) -> Self {
from.map(|s| <$ty as FromFfi>::convert(Some(s)))
}
}
};
}
impl_optional_from!(&'s [u8]);
impl_optional_from!(&'s BStr);
impl_optional_from!(&'s str);
macro_rules! impl_optional_into {
($ty:ty, $null:expr) => {
unsafe impl IntoFfi for Option<$ty> {
type Into = <$ty as IntoFfi>::Into;
fn convert(self) -> Self::Into {
self.map_or($null, <$ty as IntoFfi>::convert)
}
fn postlude(ret: &str) -> impl Display {
display!(
"if {ret}.__ptr == nil then {ret} = nil; else {}end; ",
<$ty as IntoFfi>::postlude(ret)
)
}
}
};
}
impl_optional_into!(&'static [u8], lua_buf::null());
impl_optional_into!(&'static BStr, lua_buf::null());
impl_optional_into!(&'static str, lua_buf::null());
impl_optional_into!(Vec<u8>, lua_buffer::null());
impl_optional_into!(BString, lua_buffer::null());
impl_optional_into!(String, lua_buffer::null());

View File

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

View File

@ -1,13 +1,9 @@
use crate::utils::{ffi_crate, syn_assert, syn_error};
use darling::FromMeta;
use proc_macro2::TokenStream;
use quote::{format_ident, quote, quote_spanned};
use syn::{ext::IdentExt, spanned::Spanned, *};
use quote::{format_ident, quote};
use syn::{spanned::*, *};
#[derive(Debug, FromMeta)]
pub struct Args {}
pub fn transform(_args: Args, mut item: Item) -> Result<TokenStream> {
pub fn transform(mut item: Item) -> Result<TokenStream> {
let (name, impl_type, impl_cdef) = match item {
Item::Struct(ref mut str) => (
str.ident.clone(),
@ -22,54 +18,44 @@ pub fn transform(_args: Args, mut item: Item) -> Result<TokenStream> {
_ => syn_error!(item, "expected struct or enum"),
};
let mod_name = format_ident!("__{name}_cdef");
let mod_name = format_ident!("__cdef__{name}");
Ok(quote!(
Ok(quote! {
#[repr(C)]
#[allow(non_camel_case_types)]
#item
#[doc(hidden)]
#[allow(unused, non_snake_case)]
/// Automatically generated by luaffi.
mod #mod_name {
use super::*;
#impl_type
#impl_cdef
}
))
})
}
fn generate_type(ty: &Ident) -> Result<TokenStream> {
let ffi = ffi_crate();
let span = ty.span();
let name = LitStr::new(&ty.unraw().to_string(), span);
let fmt = quote!(::std::format!);
let name_fmt = LitStr::new(&format!("{ty}"), ty.span());
let cdecl_fmt = LitStr::new(&format!("struct {ty} {{name}}"), ty.span());
Ok(quote_spanned!(span =>
Ok(quote! {
unsafe impl #ffi::Type for #ty {
fn name() -> impl ::std::fmt::Display {
#name
fn name() -> ::std::string::String {
#fmt(#name_fmt)
}
fn ty() -> #ffi::TypeType {
#ffi::TypeType::Aggregate
}
fn cdecl(name: impl ::std::fmt::Display) -> impl ::std::fmt::Display {
::std::format!("struct {} {name}", #name)
fn cdecl(name: impl ::std::fmt::Display) -> ::std::string::String {
#fmt(#cdecl_fmt)
}
fn build(b: &mut #ffi::TypeBuilder) {
b.cdef::<Self>().metatype::<Self>();
}
}
// SAFETY: we can always implement `IntoFfi` because it transfers ownership from Rust to Lua
unsafe impl #ffi::IntoFfi for #ty {
type Into = Self;
fn convert(self) -> Self::Into { self }
}
))
})
}
fn generate_cdef_structure(str: &mut ItemStruct) -> Result<TokenStream> {
@ -81,14 +67,13 @@ fn generate_cdef_structure(str: &mut ItemStruct) -> Result<TokenStream> {
let ffi = ffi_crate();
let ty = &str.ident;
let span = ty.span();
let build = generate_cdef_build(&get_cfields(&mut str.fields)?)?;
let build = generate_build_cdef(&to_cfields(&mut str.fields)?)?;
Ok(quote_spanned!(span =>
unsafe impl #ffi::Cdef for #ty {
fn build(b: &mut #ffi::CdefBuilder) { #build }
Ok(quote! {
unsafe impl #ffi::CDef for #ty {
fn build(b: &mut #ffi::CDefBuilder) { #build }
}
))
})
}
fn generate_cdef_enum(enu: &mut ItemEnum) -> Result<TokenStream> {
@ -100,24 +85,22 @@ fn generate_cdef_enum(enu: &mut ItemEnum) -> Result<TokenStream> {
let ffi = ffi_crate();
let ty = &enu.ident;
let span = ty.span();
let build = enu
.variants
.iter_mut()
.map(|variant| {
let span = variant.span();
let build = generate_cdef_build(&get_cfields(&mut variant.fields)?)?;
Ok(quote_spanned!(span => b.inner_struct(|b| { #build })))
let build = generate_build_cdef(&to_cfields(&mut variant.fields)?)?;
Ok(quote! { b.inner_struct(|b| { #build }); })
})
.collect::<Result<Vec<_>>>()?;
Ok(quote_spanned!(span =>
unsafe impl #ffi::Cdef for #ty {
fn build(b: &mut #ffi::CdefBuilder) {
b.field::<::std::ffi::c_int>("__tag").inner_union(|b| { #(#build;)* });
Ok(quote! {
unsafe impl #ffi::CDef for #ty {
fn build(b: &mut #ffi::CDefBuilder) {
b.field::<::std::ffi::c_int>("__tag").inner_union(|b| { #(#build)* });
}
}
))
})
}
struct CField {
@ -126,12 +109,7 @@ struct CField {
attrs: CFieldAttrs,
}
#[derive(Default)]
struct CFieldAttrs {
opaque: bool,
}
fn get_cfields(fields: &mut Fields) -> Result<Vec<CField>> {
fn to_cfields(fields: &mut Fields) -> Result<Vec<CField>> {
match fields {
Fields::Named(fields) => fields.named.iter_mut(),
Fields::Unnamed(fields) => fields.unnamed.iter_mut(),
@ -141,17 +119,22 @@ fn get_cfields(fields: &mut Fields) -> Result<Vec<CField>> {
.map(|(i, field)| {
Ok(CField {
name: match field.ident {
Some(ref name) => name.unraw().to_string(),
Some(ref name) => format!("{name}"),
None => format!("__{i}"),
},
ty: field.ty.clone(),
attrs: parse_cfield_attrs(&mut field.attrs)?,
attrs: parse_attrs(&mut field.attrs)?,
})
})
.collect()
}
fn parse_cfield_attrs(attrs: &mut Vec<Attribute>) -> Result<CFieldAttrs> {
#[derive(Default)]
struct CFieldAttrs {
opaque: bool,
}
fn parse_attrs(attrs: &mut Vec<Attribute>) -> Result<CFieldAttrs> {
let mut parsed = CFieldAttrs::default();
let mut i = 0;
while let Some(attr) = attrs.get(i) {
@ -168,11 +151,11 @@ fn parse_cfield_attrs(attrs: &mut Vec<Attribute>) -> Result<CFieldAttrs> {
Ok(parsed)
}
fn generate_cdef_build(fields: &[CField]) -> Result<TokenStream> {
let mut body = vec![quote!(
fn generate_build_cdef(fields: &[CField]) -> Result<TokenStream> {
let mut body = vec![quote! {
let mut offset = 0;
let mut align = 1;
)];
}];
fn offset(i: usize) -> Ident {
format_ident!("offset{i}")
@ -181,41 +164,40 @@ fn generate_cdef_build(fields: &[CField]) -> Result<TokenStream> {
let max = quote!(::std::cmp::Ord::max);
let size_of = quote!(::std::mem::size_of);
let align_of = quote!(::std::mem::align_of);
for (i, field) in fields.iter().enumerate() {
let ty = &field.ty;
let offset = offset(i);
body.push(quote_spanned!(ty.span() =>
body.push(quote! {
// round up current offset to the alignment of field type for field offset
offset = (offset + #align_of::<#ty>() - 1) & !(#align_of::<#ty>() - 1);
align = #max(align, #align_of::<#ty>());
let #offset = offset;
offset += #size_of::<#ty>();
));
});
}
body.push(quote!(
body.push(quote! {
// round up final offset to the total alignment of struct for struct size
let size = (offset + align - 1) & !(align - 1);
));
});
let len = fields.len();
for (i, field) in fields.iter().enumerate() {
let name = &field.name;
let ty = &field.ty;
body.push(if field.attrs.opaque {
if i == len - 1 {
if field.attrs.opaque {
body.push(if i == len - 1 {
let a = offset(i);
quote_spanned!(ty.span() => b.field_opaque(size - #a);) // last field
quote! { b.field_opaque(size - #a); } // last field
} else {
let a = offset(i);
let b = offset(i + 1);
quote_spanned!(ty.span() => b.field_opaque(#b - #a);)
}
} else {
quote_spanned!(ty.span() => b.field::<#ty>(#name);)
quote! { b.field_opaque(#b - #a); }
});
} else {
body.push(quote! { b.field::<#ty>(#name); });
}
}
Ok(quote!(#(#body)*))
Ok(quote! { #(#body)* })
}

View File

@ -1,4 +1,3 @@
use darling::{FromMeta, ast::NestedMeta};
use proc_macro::TokenStream as TokenStream1;
use quote::ToTokens;
use syn::parse_macro_input;
@ -9,15 +8,13 @@ mod utils;
#[proc_macro_attribute]
pub fn cdef(args: TokenStream1, input: TokenStream1) -> TokenStream1 {
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)?))
cdef::transform(parse_macro_input!(input))
.unwrap_or_else(|err| err.into_compile_error().into_token_stream())
.into()
}
#[proc_macro_attribute]
pub fn metatype(_args: TokenStream1, input: TokenStream1) -> TokenStream1 {
pub fn metatype(args: TokenStream1, input: TokenStream1) -> TokenStream1 {
metatype::transform(parse_macro_input!(input))
.unwrap_or_else(|err| err.into_compile_error().into_token_stream())
.into()

View File

@ -1,10 +1,7 @@
use crate::utils::{
ffi_crate, is_primitivelike, is_unit, pat_ident, syn_assert, syn_error, ty_name,
};
use crate::utils::{ffi_crate, is_primitive, is_unit, pat_ident, syn_assert, syn_error, ty_name};
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote, quote_spanned};
use std::{collections::HashSet, fmt, iter};
use syn::{ext::IdentExt, spanned::Spanned, *};
use quote::{format_ident, quote};
use syn::{spanned::*, *};
pub fn transform(mut imp: ItemImpl) -> Result<TokenStream> {
syn_assert!(
@ -14,182 +11,62 @@ pub fn transform(mut imp: ItemImpl) -> Result<TokenStream> {
);
let impls = generate_impls(&mut imp)?;
let mod_name = format_ident!("__{}_metatype", ty_name(&imp.self_ty)?);
let mod_name = format_ident!("__metatype__{}", ty_name(&imp.self_ty)?);
Ok(quote!(
Ok(quote! {
#imp
#[doc(hidden)]
#[allow(unused, non_snake_case)]
/// Automatically generated by luaffi.
mod #mod_name {
use super::*;
#impls
}
))
})
}
fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
let ffi = ffi_crate();
let ty = imp.self_ty.clone();
let ty_name = ty_name(&ty)?;
let mut ffi_funcs = FfiRegistry::new(ty_name.clone());
let mut lua_funcs = LuaRegistry::new(ty_name.clone());
let mut mms = HashSet::new();
let mut lua_drop = None;
let ffi_funcs = get_ffi_functions(imp)?;
let ffi_wrappers: Vec<_> = ffi_funcs
.iter()
.map(generate_ffi_wrapper)
.collect::<Result<_>>()?;
let ffi_register: Vec<_> = ffi_funcs
.iter()
.map(generate_ffi_register)
.collect::<Result<_>>()?;
for func in get_ffi_functions(imp)? {
if let Some(mm) = func.attrs.metamethod {
syn_assert!(
mms.insert(mm),
func.name,
"metamethod `{mm}` already defined"
);
}
let lua_funcs = get_lua_functions(imp)?;
let lua_register: Vec<_> = lua_funcs
.iter()
.map(generate_lua_register)
.collect::<Result<_>>()?;
add_ffi_function(&mut ffi_funcs, &func)?;
}
for func in get_lua_functions(imp)? {
if let Some(mm) = func.attrs.metamethod {
syn_assert!(
mms.insert(mm),
func.name,
"metamethod `{mm}` already defined"
);
}
if func.attrs.metamethod == Some(Metamethod::Gc) {
lua_drop = Some(func);
} else {
add_lua_function(&mut lua_funcs, &func)?;
}
}
if !mms.contains(&Metamethod::New) {
inject_fallback_new(&mut lua_funcs)?;
}
inject_merged_drop(&mut ffi_funcs, lua_drop.as_ref())?;
let ffi_shims = &ffi_funcs.shims;
let ffi_build = &ffi_funcs.build;
let lua_build = &lua_funcs.build;
let ffi_exports = generate_ffi_exports(&ffi_funcs)?;
let ty = &*imp.self_ty;
Ok(quote! {
impl #ty { #(#ffi_shims)* }
unsafe impl #ffi::Metatype for #ty {
type Target = Self;
fn build(b: &mut #ffi::MetatypeBuilder) {
#(#ffi_build)*
#(#lua_build)*
#(#ffi_register)*
#(#lua_register)*
}
}
#ffi_exports
impl #ty { #(#ffi_wrappers)* }
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Metamethod {
// known luajit metamethods (see lj_obj.h)
// index, newindex, mode and metatable are not included
Gc,
Eq,
Len,
Lt,
Le,
Concat,
Call,
Add,
Sub,
Mul,
Div,
Mod,
Pow,
Unm,
ToString,
New,
Pairs,
Ipairs,
}
impl TryFrom<&Ident> for Metamethod {
type Error = Error;
fn try_from(value: &Ident) -> Result<Self> {
Ok(match value.to_string().as_str() {
"gc" => Self::Gc,
"eq" => Self::Eq,
"len" => Self::Len,
"lt" => Self::Lt,
"le" => Self::Le,
"concat" => Self::Concat,
"call" => Self::Call,
"add" => Self::Add,
"sub" => Self::Sub,
"mul" => Self::Mul,
"div" => Self::Div,
"mod" => Self::Mod,
"pow" => Self::Pow,
"unm" => Self::Unm,
"tostring" => Self::ToString,
"new" => Self::New,
"pairs" => Self::Pairs,
"ipairs" => Self::Ipairs,
_ => syn_error!(value, "unknown metamethod"),
})
}
}
impl fmt::Display for Metamethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Self::Gc => "gc",
Self::Eq => "eq",
Self::Len => "len",
Self::Lt => "lt",
Self::Le => "le",
Self::Concat => "concat",
Self::Call => "call",
Self::Add => "add",
Self::Sub => "sub",
Self::Mul => "mul",
Self::Div => "div",
Self::Mod => "mod",
Self::Pow => "pow",
Self::Unm => "unm",
Self::ToString => "tostring",
Self::New => "new",
Self::Pairs => "pairs",
Self::Ipairs => "ipairs",
};
write!(f, "{name}")
}
}
impl ToTokens for Metamethod {
fn to_tokens(&self, tokens: &mut TokenStream) {
let name = self.to_string();
tokens.extend(quote!(#name));
}
}
struct FfiFunction {
name: Ident,
is_async: bool,
rust_name: Ident,
lua_name: String,
c_name: String,
params: Vec<PatType>,
ret: Type,
attrs: FfiFunctionAttrs,
}
#[derive(Default)]
struct FfiFunctionAttrs {
metamethod: Option<Metamethod>,
ret_out: bool,
}
fn get_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
@ -207,29 +84,33 @@ fn get_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
.sig
.inputs
.iter()
.map(|arg| match arg {
.map(|arg| {
Ok(match arg {
FnArg::Receiver(recv) => {
let ty = &recv.ty;
parse_quote_spanned!(ty.span() => self: #ty)
parse_quote! { self: #ty }
}
FnArg::Typed(ty) => ty.clone(),
})
.collect();
})
.collect::<Result<_>>()?;
let ret = match func.sig.output {
ReturnType::Default => parse_quote!(()),
ReturnType::Type(_, ref ty) => (**ty).clone(),
};
let attrs = parse_ffi_function_attrs(&mut func.attrs)?;
attrs.metamethod.map(|mm| document_metamethod(func, mm));
// whether to use out-param for return values
let ret_out = !is_primitive(&ret);
funcs.push(FfiFunction {
name: func.sig.ident.clone(),
is_async: func.sig.asyncness.is_some(),
rust_name: format_ident!("__ffi_{}", func.sig.ident),
lua_name: format!("{}", func.sig.ident),
c_name: format!("{}_{}", ty_name(&imp.self_ty)?, func.sig.ident),
params,
ret,
attrs,
ret_out,
});
}
}
@ -237,477 +118,155 @@ fn get_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
Ok(funcs)
}
fn parse_ffi_function_attrs(attrs: &mut Vec<Attribute>) -> Result<FfiFunctionAttrs> {
let mut parsed = FfiFunctionAttrs::default();
let mut i = 0;
while let Some(attr) = attrs.get(i) {
if let Some(name) = attr.path().get_ident()
&& let Ok(method) = Metamethod::try_from(name)
{
match method {
Metamethod::Gc => syn_error!(attr, "implement `Drop` instead"),
_ => {}
}
parsed.metamethod = Some(method);
attrs.remove(i);
} else {
i += 1;
}
}
Ok(parsed)
}
enum FfiParameterType {
#[derive(Debug)]
enum FfiArgType {
Default,
}
fn get_ffi_param_type(_ty: &Type) -> FfiParameterType {
FfiParameterType::Default
fn get_ffi_arg_type(ty: &Type) -> FfiArgType {
FfiArgType::Default
}
enum FfiReturnType {
Void,
ByValue,
ByOutParam,
}
fn get_ffi_ret_type(ty: &Type) -> FfiReturnType {
// aggregate type returns use an out-param instead of return by-value.
//
// out-param isn't strictly necessary (luajit can handle them fine), but luajit doesn't jit
// compile aggregate returns yet, so this is more of a performance optimisation.
// https://luajit.org/ext_ffi_semantics.html#status
//
// right now this just heuristically looks for common primitive identifiers like `i32` and
// `usize` which has its limitations when it comes to type aliases (proc-macro can't see them),
// but the worst thing that can happen with a false detection is an unnecessarily boxed
// primitive that gets just unwrapped, or an aggregate suboptimally returned by-value. it should
// be correct for 99% of rust code that isn't doing anything weird.
//
// the builder below has additional assertions to confirm whether our detection was correct.
if is_unit(ty) {
FfiReturnType::Void
} else if is_primitivelike(ty) {
FfiReturnType::ByValue
fn escape_self(name: &Ident) -> Ident {
if name == "self" {
format_ident!("__self")
} else {
FfiReturnType::ByOutParam
name.clone()
}
}
struct FfiRegistry {
ty: Ident,
shims: Vec<ImplItemFn>,
build: Vec<TokenStream>,
}
impl FfiRegistry {
fn new(ty: Ident) -> Self {
Self {
ty,
shims: vec![],
build: vec![],
}
}
}
fn add_ffi_function(registry: &mut FfiRegistry, func: &FfiFunction) -> Result<()> {
fn generate_ffi_wrapper(func: &FfiFunction) -> Result<TokenStream> {
let ffi = ffi_crate();
let ty = &registry.ty;
let func_name = &func.name;
let shim_name = format_ident!("__ffi_{}", func_name.unraw());
let lua_name = format!("{}", func_name.unraw());
let c_name = if let Some(priv_name) = lua_name.strip_prefix("__") {
format!("__{}_{priv_name}", ty.unraw())
let name = &func.name;
let rust_name = &func.rust_name;
let c_name = &func.c_name;
let mut params = vec![];
let mut args = vec![];
for param in func.params.iter() {
let name = escape_self(pat_ident(&param.pat)?);
let ty = &param.ty;
match get_ffi_arg_type(ty) {
FfiArgType::Default => {
params.push(quote! { #name: <#ty as #ffi::FromFfi>::From });
args.push(quote! { <#ty as #ffi::FromFfi>::convert(#name) });
}
}
}
// make return by out-param the first parameter
let (ret, do_ret) = if func.ret_out {
let ret = &func.ret;
params.insert(0, quote! { __ret_out: *mut #ret });
(
quote! { () },
quote! { unsafe { ::std::ptr::write(__ret_out, __ret) }; },
)
} else {
format!("{}_{lua_name}", ty.unraw())
let ret = &func.ret;
(quote! { #ret }, quote! { return __ret; })
};
let func_params = &func.params; // target function parameters
let func_ret = &func.ret; // target function return type
let mut func_args = vec![]; // target function arguments
let mut shim_params = vec![]; // shim function parameters
let mut shim_ret = if func.is_async {
// shim function return type
quote_spanned!(func_ret.span() => #ffi::future::lua_future<impl ::std::future::Future<Output = #func_ret>>)
} else {
quote_spanned!(func_ret.span() => <#func_ret as #ffi::IntoFfi>::Into)
};
let mut asserts = vec![]; // compile-time builder asserts
let mut build = vec![]; // ffi builder body
// for __new metamethods, ignore the first argument (ctype of self, for which there is no
// equivalent in C)
if func.attrs.metamethod == Some(Metamethod::New) {
build.push(quote!(
b.param_ignored();
));
}
for (i, param) in func_params.iter().enumerate() {
let func_param = &param.ty;
let shim_param = format_ident!("arg{i}");
let name = pat_ident(&param.pat)?.unraw().to_string();
match get_ffi_param_type(func_param) {
FfiParameterType::Default => {
shim_params.push(quote_spanned!(func_param.span() =>
#shim_param: <#func_param as #ffi::FromFfi>::From
));
func_args.push(quote_spanned!(param.pat.span() =>
<#func_param as #ffi::FromFfi>::convert(#shim_param)
));
build.push(quote_spanned!(param.pat.span() =>
b.param::<#func_param>(#name);
));
}
}
}
// shim function body
let mut shim_body = if func.is_async {
// for async functions, wrapped the returned future in lua_future
quote_spanned!(func_name.span() => #ffi::future::lua_future::new(Self::#func_name(#(#func_args),*)))
} else {
quote_spanned!(func_name.span() => <#func_ret as #ffi::IntoFfi>::convert(Self::#func_name(#(#func_args),*)))
};
if !func.is_async {
match get_ffi_ret_type(&func_ret) {
FfiReturnType::Void => {
asserts.push(quote_spanned!(func_ret.span() =>
<<#func_ret as #ffi::IntoFfi>::Into as #ffi::Type>::ty() == #ffi::TypeType::Void
));
}
FfiReturnType::ByValue => {
asserts.push(quote_spanned!(func_ret.span() =>
<#func_ret as #ffi::IntoFfi>::convention() == #ffi::FfiReturnConvention::ByValue
));
}
FfiReturnType::ByOutParam => {
asserts.push(quote_spanned!(func_ret.span() =>
<#func_ret as #ffi::IntoFfi>::convention() == #ffi::FfiReturnConvention::ByOutParam
));
shim_params.insert(0, quote!(out: *mut #shim_ret));
(shim_body, shim_ret) = (quote!(::std::ptr::write(out, #shim_body)), quote!(()));
}
};
}
// build.push(quote_spanned!(func_name.span() =>
// b.call_inferred(#c_name, Self::#func_name);
// ));
build.push({
let infer_args = iter::repeat_n(quote!(::std::unreachable!()), func_params.len());
let infer = if func.is_async {
quote!(|| #ffi::future::lua_future::new(Self::#func_name(#(#infer_args),*)))
} else {
quote!(|| Self::#func_name(#(#infer_args),*))
};
quote_spanned!(func_name.span() => b.call_inferred(#c_name, #infer);)
});
registry.build.push(quote_spanned!(func_name.span() =>
#(::std::assert!(#asserts);)*
));
registry.build.push(match func.attrs.metamethod {
Some(ref mm) => quote!(b.metatable(#mm, |b| { #(#build)* });),
None => quote!(b.index(#lua_name, |b| { #(#build)* });),
});
registry.shims.push(parse_quote_spanned!(func_name.span() =>
Ok(quote! {
#[unsafe(export_name = #c_name)]
unsafe extern "C" fn #shim_name(#(#shim_params),*) -> #shim_ret { #shim_body }
));
Ok(())
unsafe extern "C" fn #rust_name(#(#params),*) -> #ret {
let __ret = Self::#name(#(#args),*);
#do_ret
}
})
}
fn generate_ffi_exports(registry: &FfiRegistry) -> Result<TokenStream> {
let ty = &registry.ty;
let names = registry.shims.iter().map(|f| &f.sig.ident);
fn generate_ffi_register(func: &FfiFunction) -> Result<TokenStream> {
let ffi = ffi_crate();
let lua_name = &func.lua_name;
let c_name = &func.c_name;
let mut params = vec![];
let mut asserts = vec![];
Ok(quote_spanned!(ty.span() =>
// this ensures ffi function symbol exports are actually present in the resulting binary,
// otherwise they may get dead code-eliminated before it reaches the linker
#[used]
static __FFI_EXPORTS: &[fn()] = unsafe {
&[#(::std::mem::transmute(#ty::#names as *const ())),*]
for param in func.params.iter() {
let name = format!("{}", pat_ident(&param.pat)?);
let ty = &param.ty;
params.push(match get_ffi_arg_type(ty) {
FfiArgType::Default => quote! { b.param::<#ty>(#name); },
});
}
let ret = &func.ret;
let ret_conv = if is_unit(ret) {
quote! { #ffi::FfiReturnConvention::Void }
} else if func.ret_out {
asserts.push(quote! { #ffi::__internal::assert_type_ne_all!(#ret, ()); });
quote! { #ffi::FfiReturnConvention::OutParam }
} else {
asserts.push(quote! { #ffi::__internal::assert_type_ne_all!(#ret, ()); });
quote! { #ffi::FfiReturnConvention::ByValue }
};
))
Ok(quote! {
b.index(#lua_name, |b| {
#(#asserts)*
#(#params)*
b.call::<#ret>(#c_name, #ret_conv);
});
})
}
struct LuaFunction {
name: Ident,
params: Vec<Pat>,
name: String,
params: Vec<PatType>,
body: Block,
attrs: LuaFunctionAttrs,
}
#[derive(Default)]
struct LuaFunctionAttrs {
metamethod: Option<Metamethod>,
}
fn get_lua_functions(imp: &mut ItemImpl) -> Result<Vec<LuaFunction>> {
let mut funcs = vec![];
let mut i = 0;
for item in imp.items.iter_mut() {
if let ImplItem::Fn(func) = item
while i < imp.items.len() {
if let ImplItem::Fn(ref mut func) = imp.items[i]
&& let Some(ref abi) = func.sig.abi
&& let Some(ref abi) = abi.name
&& abi.value() == "Lua"
{
let mut params: Vec<_> = func
let params = func
.sig
.inputs
.iter()
.map(|arg| {
Ok(match arg {
FnArg::Receiver(recv) => Pat::Type(parse_quote_spanned!(recv.span() =>
self: cdata
)),
FnArg::Typed(ty) => Pat::Type(ty.clone()),
FnArg::Receiver(recv) => {
syn_assert!(ty_name(&recv.ty)? == "Self", recv, "must be `self`");
syn_assert!(recv.mutability.is_none(), recv, "cannot be mut");
parse_quote! { self: cdata }
}
FnArg::Typed(ty) => ty.clone(),
})
})
.collect::<Result<_>>()?;
if let Some(ref variadic) = func.sig.variadic {
params.push(parse_quote_spanned!(variadic.span() => variadic!()));
}
let attrs = parse_lua_function_attrs(&mut func.attrs)?;
attrs.metamethod.map(|mm| document_metamethod(func, mm));
funcs.push(LuaFunction {
name: func.sig.ident.clone(),
params,
name: format!("{}", func.sig.ident),
body: func.block.clone(),
attrs,
params,
});
stub_lua_function(func)?;
imp.items.remove(i);
} else {
i += 1;
}
}
Ok(funcs)
}
fn stub_lua_function(func: &mut ImplItemFn) -> Result<()> {
fn generate_lua_register(func: &LuaFunction) -> Result<TokenStream> {
let ffi = ffi_crate();
// converts an extern "Lua" function into a regular function with no body to allow for
// documentation generation
func.sig.abi = None;
func.attrs.push(parse_quote!(#[allow(unused)]));
func.block = parse_quote!({
::std::unreachable!("cannot call lua function from rust");
});
let inputs = &mut func.sig.inputs;
let output = &mut func.sig.output;
for input in inputs.iter_mut() {
if let FnArg::Typed(pat) = input
&& let Ok(stub) = stub_lua_type(&pat.ty)
{
pat.ty = stub.into();
}
}
if let ReturnType::Type(_, ty) = output
&& let Ok(stub) = stub_lua_type(ty)
{
*ty = stub.into();
}
if let Some(ref variadic) = func.sig.variadic {
let ty = quote_spanned!(variadic.span() => variadic);
inputs.push(parse_quote!(rest: #ffi::__internal::stub_types::#ty));
func.sig.variadic = None;
}
Ok(())
}
fn stub_lua_type(ty: &Type) -> Result<Type> {
let ffi = ffi_crate();
let span = ty.span();
let ty = if let Type::Infer(_) = ty {
quote_spanned!(span => any)
} else {
match ty_name(ty)?.to_string().as_str() {
"any" => quote_spanned!(span => any),
"nil" => quote_spanned!(span => nil),
"boolean" => quote_spanned!(span => boolean),
"lightuserdata" => quote_spanned!(span => lightuserdata),
"number" => quote_spanned!(span => number),
"integer" => quote_spanned!(span => integer),
"string" => quote_spanned!(span => string),
"table" => quote_spanned!(span => table),
"function" => quote_spanned!(span => function),
"userdata" => quote_spanned!(span => userdata),
"thread" => quote_spanned!(span => thread),
"cdata" => quote_spanned!(span => cdata),
_ => syn_error!(ty, "unknown lua type"),
}
};
Ok(parse_quote!(#ffi::__internal::stub_types::#ty))
}
fn parse_lua_function_attrs(attrs: &mut Vec<Attribute>) -> Result<LuaFunctionAttrs> {
let mut parsed = LuaFunctionAttrs::default();
let mut i = 0;
while let Some(attr) = attrs.get(i) {
if let Some(name) = attr.path().get_ident()
&& let Ok(method) = Metamethod::try_from(name)
{
match method {
Metamethod::New => syn_error!(attr, r#"cannot be applied to a lua function"#),
_ => {}
}
parsed.metamethod = Some(method);
attrs.remove(i);
} else {
i += 1;
}
}
Ok(parsed)
}
struct LuaRegistry {
ty: Ident,
build: Vec<TokenStream>,
}
impl LuaRegistry {
fn new(ty: Ident) -> Self {
Self { ty, build: vec![] }
}
}
fn add_lua_function(registry: &mut LuaRegistry, func: &LuaFunction) -> Result<()> {
let ffi = ffi_crate();
let luaify = quote!(#ffi::__internal::luaify!);
let name = func.name.unraw().to_string();
let name = &func.name;
let params = &func.params;
let body = &func.body;
registry.build.push(match func.attrs.metamethod {
Some(ref mm) => quote!(b.metatable_raw(#mm, #luaify(|#(#params),*| #body));),
None => quote!(b.index_raw(#name, #luaify(|#(#params),*| #body));),
});
Ok(())
}
fn document_metamethod(func: &mut ImplItemFn, method: Metamethod) {
let s = match method {
Metamethod::Eq => "This is a metamethod which is called by the `==` operator.".into(),
Metamethod::Len => "This is a metamethod which is called by the `#` operator.".into(),
Metamethod::Lt => "This is a metamethod which is called by the `<` operator.".into(),
Metamethod::Le => "This is a metamethod which is called by the `<=` operator.".into(),
Metamethod::Concat => "This is a metamethod which is called by the `..` operator.".into(),
Metamethod::Add => "This is a metamethod which is called by the `+` operator.".into(),
Metamethod::Sub => "This is a metamethod which is called by the `-` operator.".into(),
Metamethod::Mul => "This is a metamethod which is called by the `*` operator.".into(),
Metamethod::Div => "This is a metamethod which is called by the `/` operator.".into(),
Metamethod::Mod => "This is a metamethod which is called by the `%` operator.".into(),
Metamethod::Pow => "This is a metamethod which is called by the `^` operator.".into(),
Metamethod::Unm => "This is a metamethod which is called by the `-` operator.".into(),
Metamethod::ToString => {
"This is a metamethod which can be called by the `tostring` built-in function.".into()
}
Metamethod::Pairs => {
"This is a metamethod which can be called by the `pairs` built-in function.".into()
}
Metamethod::Ipairs => {
"This is a metamethod which can be called by the `ipairs` built-in function.".into()
}
_ => format!("This is a metamethod and cannot be called directly."),
};
func.attrs.push(parse_quote!(#[doc = ""]));
func.attrs.push(parse_quote!(#[doc = #s]));
}
fn inject_fallback_new(registry: &mut LuaRegistry) -> Result<()> {
let ty = &registry.ty;
let lua = format!(
r#"function() error("type '{}' has no constructor"); end"#,
ty.unraw(),
);
registry.build.push(quote!(
b.metatable_raw("new", #lua);
));
Ok(())
}
fn inject_merged_drop(registry: &mut FfiRegistry, lua: Option<&LuaFunction>) -> Result<()> {
let ffi = ffi_crate();
let luaify = quote!(#ffi::__internal::luaify!);
let ty = &registry.ty;
let shim_name = format_ident!("__ffi_drop");
let c_name = format_ident!("{}_drop", ty.unraw());
let c_name_str = c_name.to_string();
if let Some(lua) = lua {
syn_assert!(
lua.params.len() == 1,
lua.name,
"finaliser must take exactly one parameter"
);
syn_assert!(
pat_ident(&lua.params[0])? == "self",
lua.params[0],
"finaliser parameter must be `self`"
);
let params = &lua.params;
let body = &lua.body;
registry.build.push(quote_spanned!(ty.span() =>
if ::std::mem::needs_drop::<Self>() {
// if we have both a lua-side finaliser and a rust drop, then merge the finalisers
// by doing the lua part first then drop rust
b.declare::<#ffi::UnsafeExternCFn<(*mut Self,), ()>>(#c_name_str);
b.metatable_raw("gc", #luaify(|self| {
raw!(#luaify(#body)); // embed the lua part inside a do block
__C::#c_name(self);
}));
} else {
// we only have a lua-side finaliser
b.metatable_raw("gc", #luaify(|#(#params),*| #body));
}
));
} else {
registry.build.push(quote_spanned!(ty.span() =>
if ::std::mem::needs_drop::<Self>() {
// we only have a rust drop
b.declare::<#ffi::UnsafeExternCFn<(*mut Self,), ()>>(#c_name_str);
b.metatable_raw("gc", ::std::format_args!("__C.{}", #c_name_str));
}
));
}
registry.shims.push(parse_quote_spanned!(ty.span() =>
#[unsafe(export_name = #c_name_str)]
unsafe extern "C" fn #shim_name(ptr: *mut Self) {
unsafe { ::std::ptr::drop_in_place(ptr) }
}
));
Ok(())
Ok(quote! {
b.index_raw(#name, #ffi::__internal::luaify!(|#(#params),*| #body));
})
}

View File

@ -1,5 +1,5 @@
use std::env;
use syn::{spanned::Spanned, *};
use syn::{spanned::*, *};
macro_rules! syn_error {
($src:expr, $($fmt:expr),+) => {{
@ -10,7 +10,7 @@ macro_rules! syn_error {
macro_rules! syn_assert {
($cond:expr, $src:expr, $($fmt:expr),+) => {{
if !$cond {
crate::utils::syn_error!($src, $($fmt),+);
syn_error!($src, $($fmt),+);
}
}};
}
@ -34,15 +34,11 @@ pub fn ty_name(ty: &Type) -> Result<&Ident> {
}
}
pub fn pat_ident(pat: &Pat) -> Result<Ident> {
Ok(match pat {
Pat::Ident(ident) => match ident.subpat {
Some((_, ref subpat)) => syn_error!(subpat, "unexpected subpattern"),
None => ident.ident.clone(),
},
Pat::Wild(wild) => Ident::new("_", wild.span()),
pub fn pat_ident(pat: &Pat) -> Result<&Ident> {
match pat {
Pat::Ident(ident) => Ok(&ident.ident),
_ => syn_error!(pat, "expected ident"),
})
}
}
pub fn is_unit(ty: &Type) -> bool {
@ -55,11 +51,11 @@ pub fn is_unit(ty: &Type) -> bool {
}
}
pub fn is_primitivelike(ty: &Type) -> bool {
pub fn is_primitive(ty: &Type) -> bool {
match ty {
Type::Tuple(tuple) if tuple.elems.is_empty() => true, // unit type
Type::Reference(_) | Type::Ptr(_) => true,
Type::Paren(paren) => is_primitivelike(&paren.elem),
Type::Paren(paren) => is_primitive(&paren.elem),
Type::Path(path) => {
if let Some(name) = path.path.get_ident() {
matches!(

View File

@ -163,9 +163,9 @@ fn generate_expr_binary(f: &mut Formatter, bin: &ExprBinary, cx: Context) -> Res
| BinOp::Shl(_)
| BinOp::Shr(_)
| BinOp::Eq(_)
| BinOp::Ne(_)
| BinOp::Lt(_)
| BinOp::Le(_)
| BinOp::Ne(_)
| BinOp::Ge(_)
| BinOp::Gt(_)
| BinOp::And(_)
@ -233,22 +233,22 @@ fn generate_expr_binary(f: &mut Formatter, bin: &ExprBinary, cx: Context) -> Res
BinOp::MulAssign(_) => assign_bin_op!("*"),
BinOp::Div(_) => bin_op!("/"),
BinOp::DivAssign(_) => assign_bin_op!("/"),
BinOp::Rem(_) => call_op!("__fmod"),
BinOp::RemAssign(_) => assign_call_op!("__fmod"),
BinOp::BitAnd(_) => call_op!("__band"),
BinOp::BitAndAssign(_) => assign_call_op!("__band"),
BinOp::BitOr(_) => call_op!("__bor"),
BinOp::BitOrAssign(_) => assign_call_op!("__bor"),
BinOp::BitXor(_) => call_op!("__bxor"),
BinOp::BitXorAssign(_) => assign_call_op!("__bxor"),
BinOp::Shl(_) => call_op!("__blshift"),
BinOp::ShlAssign(_) => assign_call_op!("__blshift"),
BinOp::Shr(_) => call_op!("__barshift"),
BinOp::ShrAssign(_) => assign_call_op!("__barshift"),
BinOp::Rem(_) => call_op!("math.fmod"),
BinOp::RemAssign(_) => assign_call_op!("math.fmod"),
BinOp::BitAnd(_) => call_op!("band"),
BinOp::BitAndAssign(_) => assign_call_op!("band"),
BinOp::BitOr(_) => call_op!("bor"),
BinOp::BitOrAssign(_) => assign_call_op!("bor"),
BinOp::BitXor(_) => call_op!("bxor"),
BinOp::BitXorAssign(_) => assign_call_op!("bxor"),
BinOp::Shl(_) => call_op!("lshift"),
BinOp::ShlAssign(_) => assign_call_op!("lshift"),
BinOp::Shr(_) => call_op!("arshift"),
BinOp::ShrAssign(_) => assign_call_op!("arshift"),
BinOp::Eq(_) => bin_op!("=="),
BinOp::Ne(_) => bin_op!("~="),
BinOp::Lt(_) => bin_op!("<"),
BinOp::Le(_) => bin_op!("<="),
BinOp::Ne(_) => bin_op!("~="),
BinOp::Ge(_) => bin_op!(">="),
BinOp::Gt(_) => bin_op!(">"),
BinOp::And(_) => bin_op!("and"),
@ -523,7 +523,7 @@ fn generate_expr_tuple(f: &mut Formatter, tuple: &ExprTuple, cx: Context) -> Res
f.write("nil");
Ok(())
}
_ if cx.is_ret() || cx.is_multi_expr() => generate_punctuated_expr(f, &tuple.elems),
_ if cx.is_multi_expr() => generate_punctuated_expr(f, &tuple.elems),
_ => syn_error!(tuple, "expected single-valued expression"),
}
}
@ -569,10 +569,9 @@ fn generate_expr_while(f: &mut Formatter, whil: &ExprWhile, cx: Context) -> Resu
}
fn generate_punctuated_expr(f: &mut Formatter, exprs: &Punctuated<Expr, Token![,]>) -> Result<()> {
let len = exprs.len();
for (i, expr) in exprs.iter().enumerate() {
(i != 0).then(|| f.write(","));
generate_expr(f, expr, Context::expr(i == len - 1))?;
generate_expr(f, expr, Context::expr(false))?;
}
Ok(())
}
@ -796,8 +795,6 @@ fn generate_receiver(f: &mut Formatter, recv: &Receiver) -> Result<()> {
fn generate_macro(f: &mut Formatter, mac: &Macro, cx: Context) -> Result<()> {
match format!("{}", mac.path.require_ident()?).as_str() {
"concat" => generate_macro_concat(f, mac, cx),
"len" => generate_macro_len(f, mac, cx),
"variadic" => generate_macro_variadic(f, mac, cx),
"embed" => generate_macro_embed(f, mac, cx),
"raw" => generate_macro_raw(f, mac, cx),
name => syn_error!(mac.path, "unknown macro '{name}'"),
@ -816,24 +813,6 @@ fn generate_macro_concat(f: &mut Formatter, mac: &Macro, cx: Context) -> Result<
Ok(())
}
fn generate_macro_len(f: &mut Formatter, mac: &Macro, cx: Context) -> Result<()> {
syn_assert!(cx.is_value(), mac, "len! must be in expression position");
cx.is_ret().then(|| f.write("return"));
f.write("#");
generate_expr(f, &mac.parse_body()?, Context::expr(false))
}
fn generate_macro_variadic(f: &mut Formatter, mac: &Macro, cx: Context) -> Result<()> {
syn_assert!(
cx.is_multi_expr(),
mac,
"variadic! must be in multi-value expression position"
);
cx.is_ret().then(|| f.write("return"));
f.write("...");
Ok(())
}
fn generate_macro_embed(f: &mut Formatter, mac: &Macro, cx: Context) -> Result<()> {
syn_assert!(cx.is_value(), mac, "embed! must be in expression position");
cx.is_ret().then(|| f.write("return"));
@ -892,9 +871,9 @@ fn generate_pat_ident(f: &mut Formatter, ident: &PatIdent, _cx: PatContext) -> R
generate_ident(f, &ident.ident)
}
fn generate_pat_macro(f: &mut Formatter, mac: &PatMacro, cx: PatContext) -> Result<()> {
fn generate_pat_macro(f: &mut Formatter, mac: &PatMacro, _cx: PatContext) -> Result<()> {
assert_no_attrs!(mac);
generate_macro(f, &mac.mac, Context::expr(cx.is_multi()))
generate_macro(f, &mac.mac, Context::expr(false))
}
fn generate_pat_tuple(f: &mut Formatter, tuple: &PatTuple, cx: PatContext) -> Result<()> {
@ -921,15 +900,9 @@ fn generate_pat_wild(f: &mut Formatter, wild: &PatWild, _cx: PatContext) -> Resu
}
fn generate_punctuated_pat(f: &mut Formatter, pats: &Punctuated<Pat, Token![,]>) -> Result<()> {
let len = pats.len();
for (i, pat) in pats.iter().enumerate() {
(i != 0).then(|| f.write(","));
let cx = if i == len - 1 {
PatContext::Multi
} else {
PatContext::Single
};
generate_pat(f, pat, cx)?;
generate_pat(f, pat, PatContext::Single)?;
}
Ok(())
}

View File

@ -1,4 +1,4 @@
use crate::utils::{LuaType, expr_ident, pat_ident, syn_error, wrap_expr_block};
use crate::utils::{LuaType, syn_error, unwrap_expr_ident, unwrap_pat_ident, wrap_expr_block};
use quote::format_ident;
use std::mem;
use syn::{spanned::*, visit_mut::*, *};
@ -73,7 +73,7 @@ impl Visitor {
match input {
Pat::Ident(_) => {}
Pat::Type(typed) => {
let ident = pat_ident(&typed.pat)?;
let ident = unwrap_pat_ident(&typed.pat)?;
let ty = mem::replace(&mut typed.ty, parse_quote!(_));
match (&*ty).try_into()? {
LuaType::Any => {}
@ -112,7 +112,7 @@ impl Visitor {
Some((Ident::new("self", recv.self_token.span()), ty))
}
FnArg::Typed(typed) => {
let ident = pat_ident(&typed.pat)?;
let ident = unwrap_pat_ident(&typed.pat)?;
let ty = mem::replace(&mut typed.ty, parse_quote!(_));
Some((ident, ty))
}
@ -149,16 +149,16 @@ impl Visitor {
let mut prelude: Option<Stmt> = None;
let ty: LuaType = (&*cast.ty).try_into()?;
let ty_str = format!("{ty}");
let (ident, msg) = match expr_ident(&arg) {
Ok(ident) => (ident.clone(), format!("{ty} expected in '{ident}', got ")),
Err(_) => {
let (ident, msg) = match unwrap_expr_ident(&arg).ok() {
Some(ident) => (ident.clone(), format!("{ty} expected in '{ident}', got ")),
None => {
let ident = Ident::new("_", arg.span());
prelude = Some(parse_quote! { let #ident = #arg; });
(ident, format!("{ty} expected, got "))
}
};
let tmp = format_ident!("__{ident}");
let tmp = format_ident!("_{ident}");
let span = cast.span();
*expr = match ty {
LuaType::Any => parse_quote_spanned!(span => {}),

View File

@ -25,14 +25,14 @@ pub fn wrap_expr_block(expr: &Expr) -> Block {
}
}
pub fn expr_ident(expr: &Expr) -> Result<&Ident> {
pub fn unwrap_expr_ident(expr: &Expr) -> Result<&Ident> {
match expr {
Expr::Path(path) => path.path.require_ident(),
_ => syn_error!(expr, "expected ident"),
}
}
pub fn pat_ident(pat: &Pat) -> Result<Ident> {
pub fn unwrap_pat_ident(pat: &Pat) -> Result<Ident> {
Ok(match pat {
Pat::Ident(ident) => match ident.subpat {
Some((_, ref subpat)) => syn_error!(subpat, "unexpected subpattern"),

View File

@ -81,7 +81,7 @@ fn local_fn() {
fn check(self: string, arg: number) {}
inner
}),
r#"function()local function check(self,arg)do if type(self)=="number"then self=tostring(self);else assert(type(self)=="string","string expected in \'self\', got "..type(self));end;end;do local __arg=arg;arg=tonumber(arg);assert(arg~=nil,"number expected in \'arg\', got "..type(__arg));end;end;return inner;end"#
r#"function()local function check(self,arg)do if type(self)=="number"then self=tostring(self);else assert(type(self)=="string","string expected in \'self\', got "..type(self));end;end;do local _arg=arg;arg=tonumber(arg);assert(arg~=nil,"number expected in \'arg\', got "..type(_arg));end;end;return inner;end"#
);
}
@ -220,7 +220,7 @@ fn type_checks() {
);
assert_eq!(
luaify!(|s| { s as number }),
r#"function(s)do local __s=s;s=tonumber(s);assert(s~=nil,"number expected in \'s\', got "..type(__s));end;end"#
r#"function(s)do local _s=s;s=tonumber(s);assert(s~=nil,"number expected in \'s\', got "..type(_s));end;end"#
);
assert_eq!(
luaify!(|s| { s as nil }),
@ -279,16 +279,16 @@ fn ops() {
assert_eq!(luaify!(|| a *= b), r#"function()a=a*b;end"#);
assert_eq!(luaify!(|| a / b), r#"function()return a/b;end"#);
assert_eq!(luaify!(|| a /= b), r#"function()a=a/b;end"#);
assert_eq!(luaify!(|| a = b % c), r#"function()a=__fmod(b,c);end"#);
assert_eq!(luaify!(|| a = b << c), r#"function()a=__blshift(b,c);end"#);
assert_eq!(luaify!(|| a = b % c), r#"function()a=math.fmod(b,c);end"#);
assert_eq!(luaify!(|| a = b << c), r#"function()a=lshift(b,c);end"#);
assert_eq!(
luaify!(|| a <<= b << c),
r#"function()a=__blshift(a,__blshift(b,c));end"#
r#"function()a=lshift(a,lshift(b,c));end"#
);
assert_eq!(luaify!(|| a = b >> c), r#"function()a=__barshift(b,c);end"#);
assert_eq!(luaify!(|| a = b >> c), r#"function()a=arshift(b,c);end"#);
assert_eq!(
luaify!(|| a >>= b >> c),
r#"function()a=__barshift(a,__barshift(b,c));end"#
r#"function()a=arshift(a,arshift(b,c));end"#
);
assert_eq!(luaify!(|| a && b), r#"function()return a and b;end"#);
assert_eq!(luaify!(|| a || b), r#"function()return a or b;end"#);
@ -310,15 +310,15 @@ fn ops() {
);
assert_eq!(
luaify!(|| -a || !--b && c >> d),
r#"function()return-a or not-(-b)and __barshift(c,d);end"#
r#"function()return-a or not-(-b)and arshift(c,d);end"#
);
assert_eq!(
luaify!(|| -a || !(--b && c) >> d),
r#"function()return-a or __barshift(not(-(-b)and c),d);end"#
r#"function()return-a or arshift(not(-(-b)and c),d);end"#
);
assert_eq!(
luaify!(|| a >> b << c >> d),
r#"function()return __barshift(__blshift(__barshift(a,b),c),d);end"#
r#"function()return arshift(lshift(arshift(a,b),c),d);end"#
);
}
@ -373,32 +373,3 @@ fn ifs() {
r#"function()if a==b then c();elseif b==c then return a();else d();end;end"#
);
}
#[test]
fn variadic() {
assert_eq!(luaify!(|a, b, variadic!()| {}), r#"function(a,b,...)end"#);
assert_eq!(
luaify!(|variadic!()| {
let (a, b) = variadic!();
}),
r#"function(...)local a,b=...;end"#
);
assert_eq!(
luaify!(|a, variadic!()| {
let (a, b) = (a, b, c, variadic!());
func(a, b, (c, (d, variadic!())))
}),
r#"function(a,...)local a,b=a,b,c,...;return func(a,b,c,d,...);end"#
);
}
#[test]
fn length() {
assert_eq!(luaify!(len!(a)), r#"#a"#);
assert_eq!(
luaify!({
let (a, b, c) = (len!(a), len!(b), len!(c));
}),
r#"local a,b,c=#a,#b,#c;"#
);
}

View File

@ -1,21 +0,0 @@
[package]
name = "luajit-sys"
version = "0.1.0"
edition = "2024"
[lib]
path = "lib.rs"
[features]
default = ["jit", "ffi", "lua52"]
runtime = []
jit = []
ffi = []
unwind = []
bundled-alloc = []
lua52 = []
[build-dependencies]
bindgen = "0.71.1"
cc = "1.2.26"
which = "8.0.0"

View File

@ -1,138 +0,0 @@
#![allow(nonstandard_style)]
use std::{ffi::*, ptr};
// #[cfg(all(panic = "abort", feature = "unwind"))]
// compile_error!(r#"feature "unwind" cannot be enabled if panic = "abort""#);
// #[cfg(all(panic = "unwind", not(feature = "unwind")))]
// compile_error!(r#"feature "unwind" must be enabled if panic = "unwind""#);
include!(env!("LUAJIT_SYS_BINDGEN"));
pub unsafe extern "C" fn luaJIT_openlibs(L: *mut lua_State) {
unsafe {
lua_getglobal(L, c"package".as_ptr());
lua_getfield(L, -1, c"preload".as_ptr());
lua_replace(L, -2);
macro_rules! load {
($n:literal, $f:literal) => {{
let n: &'static CStr = $n;
let f = include_bytes!(concat!(env!("LUAJIT_SYS_JITLIB"), "/", $f));
if luaL_loadbuffer(L, f.as_ptr().cast(), f.len(), n.as_ptr()) == 0 {
lua_setfield(L, -2, n.as_ptr());
} else {
lua_error(L);
}
}};
}
load!(c"jit.vmdef", "vmdef.lua");
load!(c"jit.dis_x86", "dis_x86.lua");
load!(c"jit.dis_x64", "dis_x64.lua");
load!(c"jit.dis_arm", "dis_arm.lua");
load!(c"jit.dis_arm64", "dis_arm64.lua");
load!(c"jit.dis_arm64be", "dis_arm64be.lua");
load!(c"jit.dis_ppc", "dis_ppc.lua");
load!(c"jit.dis_mips", "dis_mips.lua");
load!(c"jit.dis_mipsel", "dis_mipsel.lua");
load!(c"jit.dis_mips64", "dis_mips64.lua");
load!(c"jit.dis_mips64el", "dis_mips64el.lua");
load!(c"jit.dis_mips64r6", "dis_mips64r6.lua");
load!(c"jit.dis_mips64r6el", "dis_mips64r6el.lua");
load!(c"jit.bc", "bc.lua");
load!(c"jit.bcsave", "bcsave.lua");
load!(c"jit.v", "v.lua");
load!(c"jit.p", "p.lua");
load!(c"jit.dump", "dump.lua");
load!(c"jit.zone", "zone.lua");
lua_pop(L, 1);
}
}
// constants not exposed by lua.h
pub const LUA_TPROTO: c_int = LUA_TTHREAD + 1;
pub const LUA_TCDATA: c_int = LUA_TTHREAD + 2;
// macros not translated by bindgen
pub unsafe fn lua_upvalueindex(i: c_int) -> c_int {
LUA_GLOBALSINDEX - i
}
pub unsafe fn lua_pop(L: *mut lua_State, n: c_int) {
unsafe { lua_settop(L, -n - 1) }
}
pub unsafe fn lua_newtable(L: *mut lua_State) {
unsafe { lua_createtable(L, 0, 0) }
}
pub unsafe fn lua_register(L: *mut lua_State, n: *const c_char, f: lua_CFunction) {
unsafe { (lua_pushcfunction(L, f), lua_setglobal(L, n)) };
}
pub unsafe fn lua_pushcfunction(L: *mut lua_State, f: lua_CFunction) {
unsafe { lua_pushcclosure(L, f, 0) }
}
pub unsafe fn lua_strlen(L: *mut lua_State, i: c_int) -> usize {
unsafe { lua_objlen(L, i) }
}
pub unsafe fn lua_isfunction(L: *mut lua_State, n: c_int) -> c_int {
unsafe { (lua_type(L, n) == LUA_TFUNCTION) as c_int }
}
pub unsafe fn lua_istable(L: *mut lua_State, n: c_int) -> c_int {
unsafe { (lua_type(L, n) == LUA_TTABLE) as c_int }
}
pub unsafe fn lua_islightuserdata(L: *mut lua_State, n: c_int) -> c_int {
unsafe { (lua_type(L, n) == LUA_TLIGHTUSERDATA) as c_int }
}
pub unsafe fn lua_isnil(L: *mut lua_State, n: c_int) -> c_int {
unsafe { (lua_type(L, n) == LUA_TNIL) as c_int }
}
pub unsafe fn lua_isboolean(L: *mut lua_State, n: c_int) -> c_int {
unsafe { (lua_type(L, n) == LUA_TBOOLEAN) as c_int }
}
pub unsafe fn lua_isthread(L: *mut lua_State, n: c_int) -> c_int {
unsafe { (lua_type(L, n) == LUA_TTHREAD) as c_int }
}
pub unsafe fn lua_isnone(L: *mut lua_State, n: c_int) -> c_int {
unsafe { (lua_type(L, n) == LUA_TNONE) as c_int }
}
pub unsafe fn lua_isnoneornil(L: *mut lua_State, n: c_int) -> c_int {
unsafe { (lua_type(L, n) <= LUA_TNIL) as c_int }
}
pub unsafe fn lua_pushliteral(L: *mut lua_State, s: impl AsRef<[u8]>) {
unsafe { lua_pushlstring(L, s.as_ref().as_ptr().cast(), s.as_ref().len()) }
}
pub unsafe fn lua_setglobal(L: *mut lua_State, s: *const c_char) {
unsafe { lua_setfield(L, LUA_GLOBALSINDEX, s) }
}
pub unsafe fn lua_getglobal(L: *mut lua_State, s: *const c_char) {
unsafe { lua_getfield(L, LUA_GLOBALSINDEX, s) }
}
pub unsafe fn lua_tostring(L: *mut lua_State, i: c_int) -> *const c_char {
unsafe { lua_tolstring(L, i, ptr::null_mut()) }
}
pub unsafe fn lua_open() -> *mut lua_State {
unsafe { luaL_newstate() }
}
pub unsafe fn lua_getregistry(L: *mut lua_State) {
unsafe { lua_pushvalue(L, LUA_REGISTRYINDEX) }
}
pub unsafe fn lua_getgccount(L: *mut lua_State) -> c_int {
unsafe { lua_gc(L, LUA_GCCOUNT, 0) }
}

View File

@ -3,13 +3,22 @@ name = "luajit"
version = "0.1.0"
edition = "2024"
[lib]
path = "lib.rs"
[features]
runtime = ["luajit-sys/runtime"]
unwind = ["luajit-sys/unwind"]
default = ["enable-jit", "enable-ffi"]
runtime = []
enable-jit = []
enable-ffi = []
[dependencies]
bitflags = { version = "2.9.1", features = ["std"] }
bstr = "1.12.0"
luaffi = { version = "0.1.0", path = "../luaffi" }
luajit-sys = { version = "0.1.0", path = "../luajit-sys" }
thiserror = "2.0.12"
[build-dependencies]
bindgen = "0.71.1"
cc = "1.2.26"
which = "8.0.0"

View File

@ -7,28 +7,23 @@ use std::{
};
use which::which;
/// Unwraps a Result and panics in a way that prints a nicer error message to the user.
macro_rules! panic_err {
($value:expr) => {{ $value.unwrap_or_else(|err| panic!("{err}")) }};
($value:expr, $($arg:expr)+) => {{ $value.unwrap_or_else(|err| panic!("{}: {err}", format_args!($($arg),+))) }};
($value:expr) => { $value.unwrap_or_else(|err| panic!("{err}")) };
($value:expr, $($arg:expr)+) => { $value.unwrap_or_else(|err| panic!("{}: {err}", format_args!($($arg),+))) };
}
/// Formats arguments in a way that is intended to address environment variables.
macro_rules! env_name {
($($arg:expr),+) => {{ format!($($arg),+).replace("-", "_").to_uppercase() }};
($($arg:expr),+) => { format!($($arg),+).replace("-", "_").to_uppercase() };
}
/// Expands to the value of an environment variable.
macro_rules! env {
($($arg:expr),+) => { panic_err!(env::var(env_name!($($arg),+))) };
}
/// Expands to the value of a cargo configuration environment variable.
macro_rules! cfg {
($($arg:expr),+) => { env!("CARGO_CFG_{}", format_args!($($arg),+)) };
}
/// Whether a cargo feature is enabled.
macro_rules! feature {
($($arg:expr),+) => { env::var_os(env_name!("CARGO_FEATURE_{}", format_args!($($arg),+))).is_some() };
}
@ -43,9 +38,8 @@ fn main() {
);
panic_err!(write(out_path.join(".relver"), get_relver()));
println!("cargo::rerun-if-changed={}", src_path.display()); // rerun if luajit source changed
println!("cargo::rerun-if-changed={}", src_path.display());
// NOTE: we cannot build bindings without building luajit first (luajit generates some headers)
build_runtime(&out_path.join("src"));
build_bindings(&out_path.join("src"));
}
@ -55,7 +49,7 @@ fn get_relver() -> String {
Command::new("git")
.args(["show", "-s", "--format=%ct"])
.output(),
"failed to obtain luajit release version (is the Git command available?)"
"failed to obtain release version (is the Git command available?)"
);
String::from_utf8_lossy(&out.stdout).into()
@ -82,7 +76,6 @@ fn copy_recursive(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()
}
fn find_make() -> Command {
// always use gnu make on bsds
let name = match parse_target(&env!("HOST")) {
(_, _, "freebsd" | "openbsd" | "netbsd" | "dragonfly", _) => "gmake",
_ => "make",
@ -107,65 +100,61 @@ fn parse_target(target: &str) -> (&str, &str, &str, Option<&str>) {
}
fn build_runtime(src_path: &Path) {
let host = env!("HOST"); // host triplet
let mut make = find_make();
let opt_level = env!("OPT_LEVEL");
let debug = opt_level == "0";
let host = env!("HOST");
let host_cc = find_cc(&host).to_str().unwrap().to_owned();
let host_ptr_width = 8 * mem::size_of::<usize>();
let target = env!("TARGET"); // target triplet
let target = env!("TARGET");
let target_cc = find_cc(&target).to_str().unwrap().to_owned();
// let target_features: HashSet<&str> = HashSet::from_iter(cfg!("target_feature").split(","));
let target_ptr_width: usize = cfg!("target_pointer_width").parse().unwrap();
let mut make = find_make();
if target == host {
println!("--- begin compile for {target}:");
} else {
println!("--- begin cross-compile on {host} for {target}:");
}
make.current_dir(src_path)
make.current_dir(&src_path)
.env_clear()
.arg("-e")
.env("PATH", env!("PATH"))
.env("MAKEFLAGS", env!("CARGO_MAKEFLAGS"))
.env("BUILDMODE", "static");
let ccopt = vec![format!("-O{} -fomit-frame-pointer", env!("OPT_LEVEL"))]; // propagate opt_level
let mut ccdebug = vec![];
let mut xcflags = vec![];
let ccopt = vec![format!("-O{opt_level} -fomit-frame-pointer")];
if env!("OPT_LEVEL") == "0" {
ccdebug.push("-g"); // generate debug information
let mut xcflags = vec![
"-DLUAJIT_ENABLE_LUA52COMPAT", // lua 5.2 compatibility
"-DLUAJIT_USE_SYSMALLOC", // disable bundled allocator
"-DLUAJIT_UNWIND_EXTERNAL -funwind-tables", // use external frame unwinding
];
if debug {
make.env("CCDEBUG", "-g"); // generate debug information
xcflags.push("-DLUAJIT_USE_GDBJIT"); // gdb support
xcflags.push("-DLUA_USE_APICHECK -DLUA_USE_ASSERT"); // enable assertions
}
xcflags.push(if feature!("unwind") {
"-DLUAJIT_UNWIND_EXTERNAL -funwind-tables" // external frame unwinding (C++ exceptions)
} else {
"-DLUAJIT_UNWIND_INTERNAL" // internal frame unwinding (setjmp/longjmp)
});
if !feature!("jit") {
if !feature!("enable-jit") {
xcflags.push("-DLUAJIT_DISABLE_JIT");
}
if !feature!("ffi") {
if !feature!("enable-ffi") {
xcflags.push("-DLUAJIT_DISABLE_FFI");
}
if !feature!("bundled-alloc") {
xcflags.push("-DLUAJIT_USE_SYSMALLOC"); // using system malloc disables the bundled allocator
}
if feature!("lua52") {
xcflags.push("-DLUAJIT_ENABLE_LUA52COMPAT");
}
// host toolchain config
match (host_ptr_width, target_ptr_width) {
(64, 64) | (32, 32) => make.env("HOST_CC", &host_cc),
(64, 32) => make.env("HOST_CC", format!("{host_cc} -m32")),
(64, 32) => make.env("HOST_CC", format!("{} -m32", host_cc)),
(n, m) if n != m => panic!("cannot cross-compile on {n}-bit host for {m}-bit target"),
(n, _) => panic!("unsupported {n}-bit architecture"),
};
// target system config
match cfg!("target_os").as_str() {
"linux" | "android" => make.env("TARGET_SYS", "Linux"),
"windows" => make.env("TARGET_SYS", "Windows"),
@ -184,7 +173,6 @@ fn build_runtime(src_path: &Path) {
_ => make.env("TARGET_SYS", "Other"),
};
// target toolchain config
if let Some(cross) = target_cc.strip_suffix("gcc") {
make.env("CC", "gcc").env("CROSS", cross);
} else if let Some(cross) = target_cc.strip_suffix("clang") {
@ -195,30 +183,16 @@ fn build_runtime(src_path: &Path) {
.env("CROSS", format!("{}/", path.parent().unwrap().display()));
}
// propagate linker config
if let Ok(path) = env::var("RUSTC_LINKER") {
if let Some(path) = env::var("RUSTC_LINKER").ok() {
make.env("TARGET_LD", panic_err!(which(path), "failed to find ld"));
}
make.env("CCOPT", ccopt.join(" "))
.env("CCDEBUG", ccdebug.join(" "))
.env("XCFLAGS", xcflags.join(" "));
if target == host {
println!("--- begin compile for {target}:");
} else {
println!("--- begin cross-compile on {host} for {target}:");
}
println!("{make:?}");
let status = panic_err!(make.status(), "failed to execute make");
(!status.success()).then(|| panic!("failed to compile luajit: {status}: {make:?}"));
println!(
"cargo::rustc-env=LUAJIT_SYS_JITLIB={}",
src_path.join("jit").display(),
);
if feature!("runtime") {
println!("cargo::rustc-link-search=native={}", src_path.display());
println!("cargo::rustc-link-lib=static=luajit");
@ -226,22 +200,12 @@ fn build_runtime(src_path: &Path) {
}
fn build_bindings(src_path: &Path) {
let mut clang_args = vec![format!("--target={}", env!("TARGET"))];
let abi = if feature!("unwind") {
clang_args.push("-fexceptions".into());
bindgen::Abi::CUnwind
} else {
clang_args.push("-fno-exceptions".into());
bindgen::Abi::C
};
let mut bindgen = bindgen::builder()
.clang_args(clang_args)
.clang_arg(format!("--target={}", env!("TARGET")))
.allowlist_item(r"^(lua|LUA).*_.+$")
.formatter(bindgen::Formatter::None)
.default_macro_constant_type(bindgen::MacroTypeVariation::Signed)
.generate_cstr(true)
.override_abi(abi, ".*");
.generate_cstr(true);
for header in ["lua.h", "lualib.h", "lauxlib.h", "luaconf.h", "luajit.h"] {
bindgen = bindgen.header(src_path.join(header).to_str().unwrap());
@ -249,5 +213,5 @@ fn build_bindings(src_path: &Path) {
let path = src_path.join("bindgen.rs");
panic_err!(panic_err!(bindgen.generate(), "failed to generate bindings").write_to_file(&path));
println!("cargo::rustc-env=LUAJIT_SYS_BINDGEN={}", path.display());
println!("cargo::rustc-env=LJ_BINDINGS={}", path.display());
}

1138
crates/luajit/lib.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
max_width = 100

View File

@ -1,2 +0,0 @@
pub use lb::fs;
pub use lb::net;

View File

@ -1,225 +1,3 @@
use clap::Parser;
use mimalloc::MiMalloc;
use owo_colors::OwoColorize;
use std::{backtrace::Backtrace, fmt::Display, net::SocketAddr, num::NonZero, panic, thread};
use sysexits::ExitCode;
#[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!(
"{}:\n{trace}",
format_args!(
"thread '{}' panicked at {location}: {msg}",
thread::current().name().unwrap_or("<unnamed>")
)
.red()
);
eprintln!(
"{}",
"This is a bug in luby. Please kindly report this at https://git.lua.re/luaneko/luby."
.yellow()
);
}
#[derive(Debug, Parser)]
struct Args {
/// Paths to scripts to execute.
#[clap(value_name = "SCRIPTS")]
path: Vec<String>,
/// Strings to execute.
#[clap(long, short = 'e', value_name = "CHUNK")]
eval: Vec<String>,
/// Libraries to require on startup.
#[clap(long, short = 'l', value_name = "NAME")]
lib: Vec<String>,
/// Console log level.
#[clap(long, value_name = "LEVEL", default_value = "debug")]
log: tracing::Level,
/// LuaJIT control commands.
#[clap(long, short = 'j', value_name = "CMD=FLAGS")]
jit: Vec<String>,
/// Number of tokio worker threads.
#[clap(long, value_name = "THREADS", default_value_t = Self::threads())]
threads: NonZero<usize>,
/// Number of tokio 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",
requires = "enable_console"
)]
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 exit_err<T, E: Display>(code: ExitCode) -> impl FnOnce(E) -> T {
move |err| {
eprintln!("{}", err.red());
code.exit()
}
}
fn main() -> Result<(), ExitCode> {
panic::set_hook(Box::new(panic_cb));
let args = Args::parse();
init_logger(&args);
let tokio = init_tokio(&args);
let lua = init_lua(&args);
let main = lua.spawn(async |s| main_async(args, s).await);
tokio.block_on(async {
lua.await;
main.await.unwrap()
})
}
fn init_logger(args: &Args) {
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{Layer, util::*};
let log = tracing_subscriber::fmt()
.compact()
.with_env_filter(
tracing_subscriber::EnvFilter::builder()
.with_default_directive(LevelFilter::from(args.log).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(log)
.init()
} else {
log.init()
}
}
fn init_tokio(args: &Args) -> tokio::runtime::Runtime {
let mut rt = match args.threads.get() {
1 => tokio::runtime::Builder::new_current_thread(),
n => {
let mut rt = tokio::runtime::Builder::new_multi_thread();
rt.worker_threads(n - 1);
rt
}
};
rt.enable_all()
.thread_name("luby")
.max_blocking_threads(args.blocking_threads.get())
.build()
.unwrap_or_else(exit_err(ExitCode::OsErr))
}
fn init_lua(args: &Args) -> lb::runtime::Runtime {
let rt = lb::runtime::Builder::new();
let mut rt = rt.build().unwrap_or_else(exit_err(ExitCode::Software));
for arg in args.jit.iter() {
let mut s = rt.guard();
if let Some((cmd, flags)) = parse_jitlib_cmd(arg)
&& let Ok(_) = s.require(format!("jit.{cmd}"), 1)
{
(s.push("start"), s.get(-2), s.push(flags));
s.call(1, 0)
} else {
s.require("jit", 1).unwrap();
match arg.as_str() {
cmd @ ("on" | "off" | "flush") => {
(s.push(cmd), s.get(-2));
s.call(0, 0)
}
arg => {
(s.push("opt"), s.get(-2));
(s.push("start"), s.get(-2), s.push(arg));
s.call(1, 0)
}
}
}
.unwrap_or_else(exit_err(ExitCode::Usage));
}
rt
}
fn parse_jitlib_cmd(s: &str) -> Option<(&str, &str)> {
match s {
"p" => Some(("p", "Flspv10")),
"v" => Some(("v", "-")),
"dump" => Some(("dump", "tirs")),
_ => s.split_once('='),
}
}
async fn main_async(args: Args, state: &mut luajit::State) -> Result<(), ExitCode> {
for ref path in args.path {
let mut s = state.guard();
let chunk = match std::fs::read(path) {
Ok(chunk) => chunk,
Err(err) => {
eprintln!("{}", format_args!("{path}: {err}").red());
ExitCode::NoInput.exit();
}
};
s.load(&luajit::Chunk::new(chunk).path(path))
.unwrap_or_else(exit_err(ExitCode::NoInput));
if let Err(err) = s.call_async(0, 0).await {
match err.trace() {
Some(trace) => eprintln!("{}\n{trace}", err.red()), // runtime error
None => eprintln!("{}", err.red()),
}
ExitCode::DataErr.exit();
}
}
Ok(())
fn main() {
println!("Hello, world!");
}

View File

@ -1,7 +0,0 @@
syntax = "LuaJIT"
indent_type = "Spaces"
indent_width = 2
column_width = 100
quote_style = "ForceDouble"
call_parentheses = "NoSingleTable"
collapse_simple_statement = "ConditionalOnly"

View File

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