Compare commits

..

90 Commits

Author SHA1 Message Date
57f391a950 Set more aggressive jit options by default 2025-06-30 15:56:45 +10:00
99aa11e903 Rename Annotation trait to Annotate 2025-06-30 15:20:40 +10:00
5c257b0f74 Update luajit crate docs 2025-06-30 06:24:37 +10:00
1808bee82a Refactor and overhaul luajit crate 2025-06-30 05:59:54 +10:00
7768c5ec56 Add basic timing library 2025-06-29 18:06:56 +10:00
fcdee34b42 Fix doc errors 2025-06-29 17:58:34 +10:00
5846220e35 Make bacon watch lua files too 2025-06-29 17:48:56 +10:00
b0efc9f783 Report test complete time 2025-06-28 21:31:42 +10:00
8c406a46b3 Correctly handle tcp disconnections 2025-06-28 21:11:09 +10:00
263ca1cf48 Add basic tcp listener test 2025-06-28 20:00:01 +10:00
505364a661 Check argument against negative sleep duration 2025-06-28 19:59:37 +10:00
d5e85f2c30 Fix race condition in task state table unref 2025-06-28 19:52:28 +10:00
cea9bc0813 Add lb_tcpsocket test 2025-06-28 17:21:28 +10:00
85ed0d8318 Add more tests for task spawn 2025-06-28 17:21:19 +10:00
eaa40ff3bc Fix all borrow errors 2025-06-28 17:20:40 +10:00
124e9bedfe Implement lb_tcpstream 2025-06-28 15:42:16 +10:00
a2cbb24a75 Turns out &mut T: FromFfi was unsound from the beginning after all 2025-06-28 15:06:32 +10:00
36300b07d3 Fix __eq metamethods throwing on type mismatch 2025-06-28 11:10:29 +10:00
db9d611ff7 Use raw equality for nil comparisons 2025-06-28 09:51:50 +10:00
a90c36a260 Add more doc to net 2025-06-28 09:36:19 +10:00
c760d12c39 Rewrite stab generation to be more extensible 2025-06-28 07:58:52 +10:00
ef811ecfa9 Use faster fxhashset in registry 2025-06-28 06:06:39 +10:00
d9ef6c7806 All internal library functions should be prefixed 2025-06-28 06:03:12 +10:00
27c40c3244 All library functions no longer require self 2025-06-28 05:58:29 +10:00
ccae0046fb Improve cdef loading time by trimming unnecessary ctype caching 2025-06-28 05:29:19 +10:00
e027623d40 Future destructor doesn't need to return nothing from drop 2025-06-28 05:22:59 +10:00
8a49321110 Don't need to fully name option and result structs 2025-06-28 04:32:53 +10:00
c70b043281 Fix main printing unnecessary ExitCode Debug impl 2025-06-28 04:18:00 +10:00
f6b91cde10 Remove type checking transformation in luaify 2025-06-28 04:15:42 +10:00
e05e2f4cb3 Implement the foundations for annotation generation 2025-06-28 04:15:27 +10:00
6a4c726965 Properly report panic by resuming unwind 2025-06-28 04:11:33 +10:00
5f1f6dab7a Improve doc 2025-06-27 23:47:52 +10:00
a760beabc1 Extern "Lua" functions should be able to act as constructors 2025-06-27 23:04:05 +10:00
5ea532f1c6 Pretty icons 2025-06-27 14:41:19 +10:00
c07ec4c3ad Add headings 2025-06-27 14:31:49 +10:00
7c40fb4322 Report number of passing and failing tests 2025-06-27 14:28:08 +10:00
eb7b05d07a Avoid globbing target dir every test 2025-06-27 04:45:53 +10:00
3a7f2366e4 Ensure spawn order is consistent 2025-06-27 04:29:11 +10:00
8443c44671 Test that task properly unrefs its table 2025-06-27 04:24:10 +10:00
72b3afaeea Add sleep test 2025-06-27 04:11:31 +10:00
503985269a Don't need to run all tests in workspace in bacon 2025-06-27 04:06:45 +10:00
0839e7ce9a Add task spawn tests 2025-06-27 04:06:35 +10:00
cdfb2522ac Add tempdir support 2025-06-27 03:53:08 +10:00
0c4639c3e9 Add basic tcp socket test 2025-06-27 03:29:29 +10:00
4f548bf9e9 More descriptive parameters 2025-06-27 03:18:25 +10:00
40829cdfc6 Implement basic lua test harness 2025-06-27 03:11:49 +10:00
db4faea821 Implement walk_dir and more robust globbing 2025-06-27 01:36:08 +10:00
2964ebbe1b Add basic glob function 2025-06-26 23:56:09 +10:00
b1572cc9f1 Rename lsdir to read_dir and dirent to dir_entry 2025-06-26 23:24:48 +10:00
862fcfe891 Add links to lua pdf in metamethod doc 2025-06-26 23:08:28 +10:00
6a194e98e8 Document structs in fslib 2025-06-26 23:08:13 +10:00
d2e06c9a70 Add stab tag for async functions 2025-06-26 22:58:30 +10:00
679ffed807 Implement lsdir and file info functions 2025-06-26 22:46:27 +10:00
549f96d4dc Warn missing documentation 2025-06-26 21:32:38 +10:00
09d7e51345 Add tokio/rt feature only if task feature is enabled 2025-06-26 21:10:07 +10:00
8a74ade6a6 Implement task handle awaiting 2025-06-26 20:18:16 +10:00
9338be7eb0 Add many lua annotation 2025-06-26 19:25:03 +10:00
240e0111bf Fix lua finaliser parameter check 2025-06-26 19:24:19 +10:00
01f459ffaf Implement task sleep function 2025-06-26 18:47:43 +10:00
9b7dbcc141 Implement proper error handlign in spawned tasks 2025-06-26 18:47:31 +10:00
24c5e9edc2 Include lb_tasklib::spawn in globals 2025-06-26 17:52:31 +10:00
0cafac0948 Let modules decide their own name via #[cdef] macro 2025-06-26 17:47:21 +10:00
1c1753234d Rename MetatypeMethodBuilder to MetatypeFunctionBuilder 2025-06-26 17:12:00 +10:00
31b5ff5ab9 Make all modules feature-gated to improve compile time 2025-06-26 16:54:58 +10:00
2078dd0d8e Delete test.lua 2025-06-26 13:57:45 +10:00
dfd57e0ad0 Add macro to luaify blocks instead of exprs 2025-06-26 13:57:10 +10:00
2a6015c19a Use pretty assertions in luaify tests 2025-06-26 13:48:54 +10:00
88ab48cddb Modules should be registered in luby not lb 2025-06-26 13:46:39 +10:00
bcb734846d Implement lb_tcpsocket 2025-06-26 13:07:24 +10:00
2fe9515702 Add newlines in generated cdef for readability 2025-06-26 13:06:39 +10:00
f456a544e1 Disable all lualsp diagnostics in lib.lua 2025-06-26 13:05:50 +10:00
ba969b9e56 Rename lb_libx to lb_xlib 2025-06-26 00:40:26 +10:00
da5acd6bc8 Add stab tag for metamethods 2025-06-26 00:39:22 +10:00
cbb47840e4 Alias stub types to rust types where possible 2025-06-25 23:16:42 +10:00
2967513cbb Allowing dumping generated cdef 2025-06-25 22:48:46 +10:00
014687068f Add file write operations 2025-06-25 22:26:43 +10:00
85a0110e1a Disable warnings in luaffi lib.lua 2025-06-25 22:19:17 +10:00
91302db725 Print nicer error messages 2025-06-25 21:45:29 +10:00
c249549b3c Fix unknown feature warning 2025-06-25 21:25:02 +10:00
30596d9331 Impl generic From/IntoFfi for Option 2025-06-25 21:24:24 +10:00
530a1530ba Call it library not module 2025-06-25 20:13:09 +10:00
a81271c0a8 No need to specify versions for local dependencies 2025-06-25 20:09:01 +10:00
7ec60f0e6e Add version command 2025-06-25 20:04:33 +10:00
82726ebb5d Set author and homepage for all packages 2025-06-25 19:57:58 +10:00
86bfc7ad34 Add read and read_sync methods 2025-06-25 18:42:40 +10:00
6cdf186b61 Add exports header to module docs 2025-06-25 18:42:29 +10:00
98100d02fa Implement string parameter specialisation 2025-06-25 18:42:09 +10:00
681dd332ab Avoid unnecessarily coping result inner value if it doesn't require ownership 2025-06-25 16:39:47 +10:00
af3dbe60cb Add Lua and FFI tags to documentation 2025-06-25 15:46:17 +10:00
a07f62ecc5 Add tracing to lb 2025-06-25 15:02:53 +10:00
49 changed files with 6090 additions and 2044 deletions

View File

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

View File

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

707
Cargo.lock generated
View File

@@ -255,6 +255,32 @@ name = "camino"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
dependencies = [
"serde",
]
[[package]]
name = "cargo-platform"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
dependencies = [
"serde",
]
[[package]]
name = "cargo_metadata"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "cc"
@@ -262,6 +288,8 @@ version = "1.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
dependencies = [
"jobserver",
"libc",
"shlex",
]
@@ -444,6 +472,46 @@ dependencies = [
"syn",
]
[[package]]
name = "deranged"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "derive_more"
version = "2.0.1"
@@ -466,6 +534,23 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "either"
version = "1.15.0"
@@ -494,6 +579,12 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "flate2"
version = "1.1.2"
@@ -510,6 +601,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -557,7 +657,19 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
@@ -566,12 +678,38 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "git2"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110"
dependencies = [
"bitflags",
"libc",
"libgit2-sys",
"log",
"url",
]
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "globset"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "h2"
version = "0.4.10"
@@ -729,12 +867,119 @@ dependencies = [
"tracing",
]
[[package]]
name = "icu_collections"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
dependencies = [
"displaydoc",
"potential_utf",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"potential_utf",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
[[package]]
name = "icu_provider"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
dependencies = [
"displaydoc",
"icu_locale_core",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -785,6 +1030,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jobserver"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
"getrandom 0.3.3",
"libc",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -793,15 +1048,18 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lb"
version = "0.1.0"
version = "0.0.1"
dependencies = [
"camino",
"derive_more",
"globset",
"luaffi",
"luaify",
"luajit",
"sysexits",
"tempfile",
"thiserror",
"tokio",
"walkdir",
]
[[package]]
@@ -810,6 +1068,18 @@ version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "libgit2-sys"
version = "0.18.2+1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222"
dependencies = [
"cc",
"libc",
"libz-sys",
"pkg-config",
]
[[package]]
name = "libloading"
version = "0.8.8"
@@ -830,6 +1100,18 @@ dependencies = [
"libc",
]
[[package]]
name = "libz-sys"
version = "1.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
@@ -837,14 +1119,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
name = "lock_api"
version = "0.4.13"
name = "litemap"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
]
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "log"
@@ -854,7 +1132,7 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "luaffi"
version = "0.1.0"
version = "0.0.1"
dependencies = [
"bstr",
"luaffi_impl",
@@ -865,7 +1143,7 @@ dependencies = [
[[package]]
name = "luaffi_impl"
version = "0.1.0"
version = "0.0.1"
dependencies = [
"darling",
"proc-macro2",
@@ -875,8 +1153,9 @@ dependencies = [
[[package]]
name = "luaify"
version = "0.1.0"
version = "0.0.1"
dependencies = [
"pretty_assertions",
"proc-macro2",
"quote",
"syn",
@@ -884,18 +1163,17 @@ dependencies = [
[[package]]
name = "luajit"
version = "0.1.0"
version = "0.0.1"
dependencies = [
"bitflags",
"bstr",
"luaffi",
"luajit-sys",
"thiserror",
]
[[package]]
name = "luajit-sys"
version = "0.1.0"
version = "0.0.1"
dependencies = [
"bindgen",
"cc",
@@ -904,7 +1182,7 @@ dependencies = [
[[package]]
name = "luby"
version = "0.1.0"
version = "0.0.1"
dependencies = [
"clap",
"console-subscriber",
@@ -916,6 +1194,7 @@ dependencies = [
"tokio",
"tracing",
"tracing-subscriber",
"vergen-git2",
]
[[package]]
@@ -976,7 +1255,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
@@ -1000,6 +1279,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -1009,6 +1294,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.36.7"
@@ -1038,32 +1332,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owo-colors"
version = "4.2.1"
version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26995317201fa17f3656c36716aed4a7c81743a9634ac4c99c0eeda495db0cec"
[[package]]
name = "parking_lot"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
]
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
[[package]]
name = "percent-encoding"
@@ -1103,6 +1374,27 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "potential_utf"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1112,6 +1404,16 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "prettyplease"
version = "0.2.35"
@@ -1172,6 +1474,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
@@ -1199,16 +1507,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
dependencies = [
"bitflags",
"getrandom 0.2.16",
]
[[package]]
@@ -1267,6 +1566,15 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.0.7"
@@ -1293,10 +1601,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scopeguard"
version = "1.2.0"
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "semver"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
dependencies = [
"serde",
]
[[package]]
name = "serde"
@@ -1345,15 +1665,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
dependencies = [
"libc",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
@@ -1382,6 +1693,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.11.1"
@@ -1405,12 +1722,36 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sysexits"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "198f60d1f7f003f168507691e42d082df109ef0f05c6fd006e22528371a5f1b4"
[[package]]
name = "tempfile"
version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "2.0.12"
@@ -1440,6 +1781,49 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tokio"
version = "1.45.1"
@@ -1450,9 +1834,7 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
@@ -1655,6 +2037,23 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "url"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
@@ -1667,6 +2066,63 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vergen"
version = "9.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777"
dependencies = [
"anyhow",
"cargo_metadata",
"derive_builder",
"regex",
"rustc_version",
"rustversion",
"vergen-lib",
]
[[package]]
name = "vergen-git2"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f6ee511ec45098eabade8a0750e76eec671e7fb2d9360c563911336bea9cac1"
dependencies = [
"anyhow",
"derive_builder",
"git2",
"rustversion",
"time",
"vergen",
"vergen-lib",
]
[[package]]
name = "vergen-lib"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166"
dependencies = [
"anyhow",
"derive_builder",
"rustversion",
]
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
@@ -1682,6 +2138,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "which"
version = "8.0.0"
@@ -1709,6 +2174,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@@ -1876,6 +2350,51 @@ version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "writeable"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yoke"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.8.26"
@@ -1895,3 +2414,57 @@ dependencies = [
"quote",
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerotrie"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@@ -1,4 +1,5 @@
[workspace]
resolver = "3"
members = [
"crates/lb",
"crates/luaffi",
@@ -8,23 +9,50 @@ members = [
"crates/luajit-sys",
]
[workspace.package]
version = "0.0.1"
edition = "2024"
license = "MIT"
authors = ["luaneko <lumi@lua.re>"]
homepage = "https://git.lua.re/luaneko/luby/"
repository = "https://git.lua.re/luaneko/luby/"
[package]
name = "luby"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
[profile]
dev.panic = "abort"
release.panic = "abort"
[package]
name = "luby"
version = "0.1.0"
edition = "2024"
[[test]]
name = "main"
harness = false
[features]
default = ["task", "time", "fs", "net"]
task = ["lb/task"]
time = ["lb/time"]
fs = ["lb/fs"]
net = ["lb/net"]
tokio-console = ["dep:console-subscriber"]
[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"] }
clap = { version = "4.5.40", features = ["derive", "env"] }
console-subscriber = { version = "0.4.1", optional = true }
lb = { path = "crates/lb", features = ["runtime"] }
luajit = { 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"] }
tokio = { version = "1.45.1", features = ["rt", "rt-multi-thread"] }
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
[build-dependencies]
vergen-git2 = { version = "1.0.7", features = ["cargo", "rustc"] }

View File

@@ -1,6 +1,10 @@
default_job = "test"
[jobs.test]
command = ["cargo", "test", "--workspace"]
command = ["cargo", "test"]
watch = ["*.lua"]
need_stdout = true
background = false
[jobs.doc]
command = ["cargo", "doc", "--workspace"]

13
build.rs Normal file
View File

@@ -0,0 +1,13 @@
use vergen_git2::{CargoBuilder, Emitter, Git2Builder, RustcBuilder};
fn main() {
Emitter::default()
.add_instructions(&CargoBuilder::all_cargo().unwrap())
.unwrap()
.add_instructions(&Git2Builder::all_git().unwrap())
.unwrap()
.add_instructions(&RustcBuilder::all_rustc().unwrap())
.unwrap()
.emit()
.unwrap();
}

View File

@@ -1,16 +1,27 @@
[package]
name = "lb"
version = "0.1.0"
edition = "2024"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
[features]
runtime = ["tokio/rt"]
task = ["tokio/rt", "tokio/time"]
time = []
fs = ["tokio/fs", "dep:walkdir", "dep:globset", "dep:tempfile"]
net = ["tokio/net", "tokio/io-util"]
[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]
globset = { version = "0.4.16", optional = true }
luaffi = { path = "../luaffi" }
luaify = { path = "../luaify" }
tokio = { version = "1.45.1", features = ["full"] }
luajit = { path = "../luajit" }
sysexits = "0.9.0"
tempfile = { version = "3.20.0", optional = true }
thiserror = "2.0.12"
tokio = { version = "1.45.1" }
walkdir = { version = "2.5.0", optional = true }

View File

@@ -1,11 +1,26 @@
// use flume::{Receiver, Sender};
//! # Channel library
//!
//! The `lb:chan` library provides primitives for asynchronous communication between tasks via
//! message passing channels.
//!
//! ## Exports
//!
//! See [`lb_chanlib`] for items exported by this library.
use luaffi::{cdef, metatype};
#[cdef]
pub struct lb_libchannel;
/// Items exported by the `lb:chan` library.
///
/// This library can be acquired by calling
/// [`require("lb:chan")`](https://www.lua.org/manual/5.1/manual.html#pdf-require).
///
/// ```lua
/// local chan = require("lb:chan");
/// ```
#[cdef(module = "lb:chan")]
pub struct lb_chanlib;
#[metatype]
impl lb_libchannel {
impl lb_chanlib {
#[new]
extern "Lua-C" fn new() -> Self {
Self
@@ -17,7 +32,7 @@ impl lb_libchannel {
(send, recv)
}
extern "Lua" fn bounded(self, cap: number) {
extern "Lua" fn bounded(self, cap: u32) {
assert(cap >= 0, "channel capacity must be nonnegative");
let (send, recv) = (__new(__ct.lb_sender), __new(__ct.lb_receiver));
self.__bounded(cap, send, recv);

View File

@@ -1,32 +1,645 @@
//! The `lb:fs` module provides utilities for interacting with the file system asynchronously.
//! # Filesystem library
//!
//! See [`lb_libfs`] for items exported by this module.
//! The `lb:fs` library provides synchronous and asynchronous utilities for interacting with the
//! filesystem.
//!
//! ## Asynchronous by default
//!
//! Filesystem operations are blocking by nature; to provide asynchronicity, luby performs blocking
//! operations in a background thread pool by default. Synchronous complements to all asynchronous
//! functions are always provided.
//!
//! ## Exports
//!
//! See [`lb_fslib`] for items exported by this library.
use luaffi::{cdef, metatype};
use std::io;
use tokio::fs;
use std::{
cell::{BorrowError, BorrowMutError, RefCell},
path::PathBuf,
time::SystemTime,
};
use thiserror::Error;
/// Items exported by the `lb:fs` module.
/// Errors that can be thrown by this library.
///
/// This module can be obtained by calling `require` in Lua.
/// Functions which return this error will **throw**. The error message can be caught by using
/// [`pcall(f, ...)`](https://www.lua.org/manual/5.1/manual.html#pdf-pcall).
#[derive(Debug, Error)]
pub enum Error {
/// Attempt to access an object while it is being modified.
#[error("cannot access object while it is being modified")]
Borrow(#[from] BorrowError),
/// Attempt to modify an object while it is in use.
#[error("cannot modify object while it is in use")]
BorrowMut(#[from] BorrowMutError),
/// I/O error.
#[error("{0}")]
Io(#[from] std::io::Error),
/// Walk directory error.
#[error("{0}")]
Walk(#[from] walkdir::Error),
/// Glob pattern error.
#[error("{0}")]
Glob(#[from] globset::Error),
}
type Result<T> = std::result::Result<T, Error>;
/// Items exported by the `lb:fs` library.
///
/// This library can be acquired by calling
/// [`require("lb:fs")`](https://www.lua.org/manual/5.1/manual.html#pdf-require).
///
/// ```lua
/// local fs = require("lb:fs");
/// ```
#[cdef]
pub struct lb_libfs;
#[cdef(module = "lb:fs")]
pub struct lb_fslib;
#[metatype]
impl lb_libfs {
impl lb_fslib {
#[new]
extern "Lua-C" fn new() -> Self {
Self
}
pub extern "Lua" fn read(&self, path: string) -> string {
self.__read(path)
/// Reads the entire contents of a file.
///
/// # Errors
///
/// This function may throw if the file does not exist or could not be read.
pub async extern "Lua-C" fn read(path: &str) -> Result<Vec<u8>> {
Ok(tokio::fs::read(path).await?)
}
async extern "Lua-C" fn __read(&self, path: &str) -> io::Result<Vec<u8>> {
fs::read(path).await
/// Reads the entire contents of a file synchronously.
///
/// This is a synchronous complement to [`read`](Self::read).
///
/// # Errors
///
/// This function may throw if the file does not exist or could not be read.
pub extern "Lua-C" fn read_sync(path: &str) -> Result<Vec<u8>> {
Ok(std::fs::read(path)?)
}
/// Writes the given contents to a file, replacing its contents if it exists.
///
/// # Errors
///
/// This function may throw if the file could not be written.
pub async extern "Lua-C" fn write(path: &str, contents: &[u8]) -> Result<()> {
Ok(tokio::fs::write(path, contents).await?)
}
/// Writes the given contents to a file synchronously, replacing its contents if it exists.
///
/// This is a synchronous complement to [`write`](Self::write).
///
/// # Errors
///
/// This function may throw if the file could not be written.
pub extern "Lua-C" fn write_sync(path: &str, contents: &[u8]) -> Result<()> {
Ok(std::fs::write(path, contents)?)
}
/// Reads the entries in a directory.
///
/// # Errors
///
/// This function may throw if the directory could not be read.
pub async extern "Lua-C" fn read_dir(path: &str) -> Result<lb_read_dir> {
Ok(lb_read_dir::new(tokio::fs::read_dir(path).await?))
}
/// Reads the entries in a directory synchronously.
///
/// This is a synchronous complement to [`read_dir`](Self::read_dir).
///
/// # Errors
///
/// This function may throw if the directory could not be read.
pub extern "Lua-C" fn read_dir_sync(path: &str) -> Result<lb_read_dir_sync> {
Ok(lb_read_dir_sync::new(std::fs::read_dir(path)?))
}
/// Recursively walks a directory, yielding all entries within it.
pub extern "Lua-C" fn walk_dir(path: &str) -> lb_walk_dir {
lb_walk_dir::new(walkdir::WalkDir::new(path).into_iter())
}
/// Returns an iterator over all files matching a glob pattern in the current directory.
///
/// # Errors
///
/// This function may throw if the pattern is invalid.
pub extern "Lua" fn glob(pattern: &str) -> Result<lb_glob_dir> {
Self::glob_dir(".", pattern)
}
/// Returns an iterator over all files matching a glob pattern in the given directory.
///
/// # Errors
///
/// This function may throw if the pattern is invalid.
pub extern "Lua-C" fn glob_dir(path: &str, pattern: &str) -> Result<lb_glob_dir> {
let prefix = PathBuf::from(path);
let iter = walkdir::WalkDir::new(path).min_depth(1).into_iter();
let matcher = globset::GlobSet::builder()
.add(globset::Glob::new(pattern)?)
.build()?;
Ok(lb_glob_dir::new(iter, matcher, prefix))
}
/// Creates a new temporary directory.
///
/// # Errors
///
/// This function may throw if the temporary directory could not be created.
pub extern "Lua-C" fn temp_dir() -> Result<lb_temp_dir> {
Ok(lb_temp_dir::new(tempfile::tempdir()?))
}
/// Creates a new temporary directory inside the specified path.
///
/// # Errors
///
/// This function may throw if the temporary directory could not be created.
pub extern "Lua-C" fn temp_dir_in(path: &str) -> Result<lb_temp_dir> {
Ok(lb_temp_dir::new(tempfile::tempdir_in(path)?))
}
}
/// Iterator over the entries in a directory.
#[derive(Debug)]
#[cdef]
pub struct lb_read_dir(#[opaque] RefCell<tokio::fs::ReadDir>);
#[metatype]
impl lb_read_dir {
fn new(iter: tokio::fs::ReadDir) -> Self {
Self(RefCell::new(iter))
}
/// Returns the next entry in the directory, or `nil` if there are no more entries.
///
/// # Errors
///
/// This function may throw if the directory could not be read.
#[call]
pub async extern "Lua-C" fn next(&self) -> Result<Option<lb_dir_entry>> {
Ok(self
.0
.try_borrow_mut()?
.next_entry()
.await?
.map(lb_dir_entry::new))
}
}
/// Synchronous version of [`lb_read_dir`].
#[derive(Debug)]
#[cdef]
pub struct lb_read_dir_sync(#[opaque] RefCell<std::fs::ReadDir>);
#[metatype]
impl lb_read_dir_sync {
fn new(iter: std::fs::ReadDir) -> Self {
Self(RefCell::new(iter))
}
/// Returns the next entry in the directory, or `nil` if there are no more entries.
///
/// # Errors
///
/// This function may throw if the directory could not be read.
#[call]
pub extern "Lua-C" fn next(&self) -> Result<Option<lb_dir_entry_sync>> {
Ok(self
.0
.try_borrow_mut()?
.next()
.transpose()?
.map(lb_dir_entry_sync::new))
}
}
/// Entry inside of a directory on the filesystem.
#[derive(Debug)]
#[cdef]
pub struct lb_dir_entry(#[opaque] tokio::fs::DirEntry);
#[metatype]
impl lb_dir_entry {
fn new(entry: tokio::fs::DirEntry) -> Self {
Self(entry)
}
/// Returns the full path of this entry.
pub extern "Lua-C" fn path(&self) -> String {
self.0.path().to_string_lossy().into()
}
/// Returns the file name of this entry.
pub extern "Lua-C" fn name(&self) -> String {
self.0.file_name().to_string_lossy().into()
}
/// Returns the type of this entry.
///
/// # Errors
///
/// This function may throw if the file type could not be determined.
pub async extern "Lua-C" fn r#type(&self) -> Result<lb_file_type> {
Ok(lb_file_type::new(self.0.file_type().await?))
}
/// Returns the metadata for this entry.
///
/// # Errors
///
/// This function may throw if the metadata could not be retrieved.
pub async extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> {
Ok(lb_file_meta::new(self.0.metadata().await?))
}
/// Returns the inode number for this entry.
#[cfg(unix)]
pub extern "Lua-C" fn ino(&self) -> u64 {
self.0.ino()
}
/// Returns the full path of this entry.
#[tostring]
pub extern "Lua" fn tostring(&self) -> String {
self.path()
}
}
/// Synchronous version of [`lb_dir_entry`].
#[derive(Debug)]
#[cdef]
pub struct lb_dir_entry_sync(#[opaque] std::fs::DirEntry);
#[metatype]
impl lb_dir_entry_sync {
fn new(entry: std::fs::DirEntry) -> Self {
Self(entry)
}
/// Returns the full path of this entry.
pub extern "Lua-C" fn path(&self) -> String {
self.0.path().to_string_lossy().into()
}
/// Returns the file name of this entry.
pub extern "Lua-C" fn name(&self) -> String {
self.0.file_name().to_string_lossy().into()
}
/// Returns the type of this entry.
///
/// # Errors
///
/// This function may throw if the file type could not be determined.
pub extern "Lua-C" fn r#type(&self) -> Result<lb_file_type> {
Ok(lb_file_type::new(self.0.file_type()?))
}
/// Returns the metadata for this entry.
///
/// # Errors
///
/// This function may throw if the metadata could not be retrieved.
pub extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> {
Ok(lb_file_meta::new(self.0.metadata()?))
}
/// Returns the inode number for this entry.
#[cfg(unix)]
pub extern "Lua-C" fn ino(&self) -> u64 {
use std::os::unix::fs::DirEntryExt;
self.0.ino()
}
/// Returns the full path of this entry.
#[tostring]
pub extern "Lua" fn tostring(&self) -> String {
self.path()
}
}
/// Structure representing the type of a file with accessors for each file type.
#[derive(Debug)]
#[cdef]
pub struct lb_file_type(#[opaque] std::fs::FileType);
#[metatype]
impl lb_file_type {
fn new(ty: std::fs::FileType) -> Self {
Self(ty)
}
/// Returns `true` if this file type is a directory.
pub extern "Lua-C" fn is_dir(&self) -> bool {
self.0.is_dir()
}
/// Returns `true` if this file type is a regular file.
pub extern "Lua-C" fn is_file(&self) -> bool {
self.0.is_file()
}
/// Returns `true` if this file type is a symbolic link.
pub extern "Lua-C" fn is_symlink(&self) -> bool {
self.0.is_file()
}
/// Returns the string `"file"` if this is a regular file, `"dir"` if this is a directory,
/// `"symlink"` if this is a symbolic link, or `"other"` if it is some other type of file.
#[tostring]
pub extern "Lua-C" fn tostring(&self) -> String {
if self.0.is_file() {
"file"
} else if self.0.is_dir() {
"dir"
} else if self.0.is_symlink() {
"symlink"
} else {
"other"
}
.into()
}
}
/// Metadata information about a file.
#[derive(Debug)]
#[cdef]
pub struct lb_file_meta(#[opaque] std::fs::Metadata);
#[metatype]
impl lb_file_meta {
fn new(meta: std::fs::Metadata) -> Self {
Self(meta)
}
/// Returns `true` if this file is a directory.
pub extern "Lua-C" fn is_dir(&self) -> bool {
self.0.is_dir()
}
/// Returns `true` if this file is a regular file.
pub extern "Lua-C" fn is_file(&self) -> bool {
self.0.is_file()
}
/// Returns `true` if this file is a symbolic link.
pub extern "Lua-C" fn is_symlink(&self) -> bool {
self.0.is_file()
}
/// Returns the type of this file.
pub extern "Lua-C" fn r#type(&self) -> lb_file_type {
lb_file_type::new(self.0.file_type())
}
/// Returns the size of this file in bytes.
pub extern "Lua-C" fn size(&self) -> u64 {
self.0.len()
}
/// Returns the permissions of this file.
pub extern "Lua-C" fn perms(&self) -> lb_file_perms {
lb_file_perms::new(self.0.permissions())
}
/// Returns the creation time of this file as seconds since the Unix epoch.
///
/// # Errors
///
/// This function may throw if the creation time could not be retrieved.
pub extern "Lua-C" fn created(&self) -> Result<f64> {
Ok(self
.0
.created()?
.duration_since(SystemTime::UNIX_EPOCH)
.map(|dur| dur.as_secs_f64())
.unwrap_or(0.))
}
/// Returns the modification time of this file as seconds since the Unix epoch.
///
/// # Errors
///
/// This function may throw if the modification time could not be retrieved.
pub extern "Lua-C" fn modified(&self) -> Result<f64> {
Ok(self
.0
.modified()?
.duration_since(SystemTime::UNIX_EPOCH)
.map(|dur| dur.as_secs_f64())
.unwrap_or(0.))
}
/// Returns the last access time of this file as seconds since the Unix epoch.
///
/// # Errors
///
/// This function may throw if the access time could not be retrieved.
pub extern "Lua-C" fn accessed(&self) -> Result<f64> {
Ok(self
.0
.accessed()?
.duration_since(SystemTime::UNIX_EPOCH)
.map(|dur| dur.as_secs_f64())
.unwrap_or(0.))
}
/// Returns a string representation of this file's metadata.
#[tostring]
pub extern "Lua-C" fn tostring(&self) -> String {
let ty = self.0.file_type();
if ty.is_file() {
format!("file {}", self.0.len())
} else if ty.is_dir() {
"dir".into()
} else if ty.is_symlink() {
"symlink".into()
} else {
"other".into()
}
}
}
/// Representation of the various permissions on a file.
#[derive(Debug)]
#[cdef]
pub struct lb_file_perms(#[opaque] RefCell<std::fs::Permissions>);
#[metatype]
impl lb_file_perms {
fn new(perms: std::fs::Permissions) -> Self {
Self(RefCell::new(perms))
}
/// Returns `true` if the readonly flag is set.
pub extern "Lua-C" fn readonly(&self) -> bool {
self.0.borrow().readonly()
}
/// Sets the readonly flag.
pub extern "Lua-C" fn set_readonly(&self, readonly: bool) {
self.0.borrow_mut().set_readonly(readonly);
}
}
/// Iterator for recursively descending into a directory.
#[derive(Debug)]
#[cdef]
pub struct lb_walk_dir(#[opaque] RefCell<walkdir::IntoIter>);
#[metatype]
impl lb_walk_dir {
fn new(iter: walkdir::IntoIter) -> Self {
Self(RefCell::new(iter))
}
/// Returns the next entry in the walk, or `nil` if there are no more entries.
///
/// # Errors
///
/// This function may throw if the directory could not be read.
#[call]
pub extern "Lua-C" fn next(&self) -> Result<Option<lb_walk_dir_entry>> {
Ok(self
.0
.try_borrow_mut()?
.next()
.transpose()?
.map(lb_walk_dir_entry::new))
}
}
/// Entry inside of a directory on the filesystem obtained from [`lb_walk_dir`].
#[derive(Debug)]
#[cdef]
pub struct lb_walk_dir_entry(#[opaque] walkdir::DirEntry);
#[metatype]
impl lb_walk_dir_entry {
fn new(entry: walkdir::DirEntry) -> Self {
Self(entry)
}
/// Returns the full path of this entry.
pub extern "Lua-C" fn path(&self) -> String {
self.0.path().to_string_lossy().into()
}
/// Returns the file name of this entry.
pub extern "Lua-C" fn name(&self) -> String {
self.0.file_name().to_string_lossy().into()
}
/// Returns the type of this entry.
pub extern "Lua-C" fn r#type(&self) -> lb_file_type {
lb_file_type::new(self.0.file_type())
}
/// Returns the metadata for this entry.
///
/// # Errors
///
/// This function may throw if the metadata could not be retrieved.
pub extern "Lua-C" fn metadata(&self) -> Result<lb_file_meta> {
Ok(lb_file_meta::new(self.0.metadata()?))
}
/// Returns `true` if this entry was created from a symbolic link.
pub extern "Lua-C" fn is_symlink(&self) -> bool {
self.0.path_is_symlink()
}
/// Returns the depth of this entry in the walk.
pub extern "Lua-C" fn depth(&self) -> u32 {
self.0.depth() as u32
}
/// Returns the inode number for this entry.
#[cfg(unix)]
pub extern "Lua-C" fn ino(&self) -> u64 {
use walkdir::DirEntryExt;
self.0.ino()
}
/// Returns the full path of this entry.
#[tostring]
pub extern "Lua" fn tostring(&self) -> String {
self.path()
}
}
/// Iterator that yields paths from the filesystem that match a particular pattern.
#[derive(Debug)]
#[cdef]
pub struct lb_glob_dir {
#[opaque]
iter: RefCell<walkdir::IntoIter>,
#[opaque]
matcher: globset::GlobSet,
#[opaque]
prefix: PathBuf,
}
#[metatype]
impl lb_glob_dir {
fn new(iter: walkdir::IntoIter, matcher: globset::GlobSet, prefix: PathBuf) -> Self {
Self {
iter: RefCell::new(iter),
matcher,
prefix,
}
}
/// Returns the next entry matching the glob pattern, or `nil` if there are no more entries.
///
/// # Errors
///
/// This function may throw if the directory could not be read.
#[call]
pub extern "Lua-C" fn next(&self) -> Result<Option<lb_walk_dir_entry>> {
while let Some(res) = self.iter.try_borrow_mut()?.next() {
let entry = res?;
let path = entry.path().strip_prefix(&self.prefix).unwrap();
if self.matcher.is_match(path) {
return Ok(Some(lb_walk_dir_entry::new(entry)));
}
}
Ok(None)
}
}
/// Directory in the filesystem that is automatically deleted when it is garbage-collected.
#[derive(Debug)]
#[cdef]
pub struct lb_temp_dir(#[opaque] tempfile::TempDir);
#[metatype]
impl lb_temp_dir {
fn new(dir: tempfile::TempDir) -> Self {
Self(dir)
}
/// Returns the full path of this temporary directory.
pub extern "Lua-C" fn path(&self) -> String {
self.0.path().to_string_lossy().into()
}
/// Returns the full path of this temporary directory.
#[tostring]
pub extern "Lua" fn tostring(&self) -> String {
self.path()
}
}

View File

@@ -1,5 +1,13 @@
pub mod channel;
//! luby standard library
#[cfg(feature = "task")]
pub mod chan;
#[cfg(feature = "fs")]
pub mod fs;
#[cfg(feature = "net")]
pub mod net;
#[cfg(feature = "runtime")]
pub mod runtime;
#[cfg(feature = "task")]
pub mod task;
#[cfg(feature = "time")]
pub mod time;

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,85 +1,206 @@
use crate::{channel::lb_libchannel, fs::lb_libfs, net::lb_libnet, task::lb_libtask};
#![doc(hidden)]
use derive_more::{Deref, DerefMut};
use luaffi::{Registry, Type};
use luajit::{Chunk, State};
use std::fmt::Display;
use luaffi::{Module, Registry};
use luaify::luaify_chunk;
use luajit::{Chunk, Index, NewTable, State};
use std::rc::Rc;
use tokio::{
task::{JoinHandle, LocalSet, futures::TaskLocalFuture, spawn_local},
task_local,
};
#[derive(Debug, Default)]
pub type ErrorFn = dyn Fn(&luajit::Error);
pub struct Builder {
registry: Registry,
report_err: Rc<ErrorFn>,
jit_opts: Vec<String>,
prohibit_globals: bool,
}
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
Self {
registry: Registry::new(),
report_err: Rc::new(|err| match err.trace() {
Some(trace) => eprintln!("unhandled lua error: {err}\n{trace}"),
None => eprintln!("unhandled lua error: {err}"),
}),
jit_opts: vec![
// more aggressive jit options based on OpenResty's defaults
// https://github.com/openresty/luajit2#updated-jit-default-parameters
"maxtrace=8000".into(),
"maxrecord=16000".into(),
"minstitch=3".into(),
"maxmcode=40960".into(),
],
prohibit_globals: false,
}
}
pub fn registry(&self) -> &Registry {
&self.registry
}
pub fn unhandled_error(&mut self, handler: impl Fn(&luajit::Error) + 'static) -> &mut Self {
self.report_err = Rc::new(handler);
self
}
pub fn prohibit_globals(&mut self, enabled: bool) -> &mut Self {
self.prohibit_globals = enabled;
self
}
pub fn jit_opt(&mut self, opt: impl AsRef<str>) -> &mut Self {
self.jit_opts.push(opt.as_ref().into());
self
}
pub fn jit_opts(&mut self, opts: impl IntoIterator<Item: AsRef<str>>) -> &mut Self {
for opt in opts {
self.jit_opt(opt);
}
self
}
pub fn module<T: Module>(&mut self) -> &mut Self {
self.registry.preload::<T>();
self
}
pub fn build(&self) -> luajit::Result<Runtime> {
let mut state = State::new()?;
let chunk = Chunk::new(self.registry.build()).with_path("[luby]");
state.eval(&chunk, 0, Some(0))?;
if self.prohibit_globals {
state_prohibit_globals(&mut state)?;
}
for opt in self.jit_opts.iter() {
state_set_jitopt(&mut state, opt)?;
}
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
cx: Context {
state,
report_err: self.report_err.clone(),
},
tasks: LocalSet::new(),
})
}
}
#[derive(Debug, Deref, DerefMut)]
fn state_prohibit_globals(state: &mut State) -> luajit::Result<()> {
let mut s = state.guard();
let chunk = Chunk::new(luaify_chunk!({
return |self, key, value| {
error(("undeclared local variable '%s'").format(key), 2);
};
}))
.with_path("[luby]");
s.eval(&chunk, 0, Some(1)).unwrap();
s.push(NewTable::new());
(s.push("__index"), s.push_idx(-3), s.set(-3));
(s.push("__newindex"), s.push_idx(-3), s.set(-3));
s.set_metatable(Index::globals());
Ok(())
}
fn state_set_jitopt(state: &mut State, opt: &str) -> luajit::Result<()> {
let mut s = state.guard();
if let Some((cmd, opt)) = parse_jitlib_cmd(opt)
&& let Ok(_) = s.require(format!("jit.{cmd}"), Some(1))
{
// require("jit.{cmd}").start(opt)
(s.push("start"), s.get(-2));
s.push(opt);
s.call(1, Some(0))?;
} else {
s.require("jit", Some(1)).unwrap();
match opt {
cmd @ ("on" | "off" | "flush") => {
// require("jit").<on|off|flush>()
(s.push(cmd), s.get(-2));
s.call(0, Some(0))?;
}
_ => {
// require("jit").opt.start(opt)
(s.push("opt"), s.get(-2));
(s.push("start"), s.get(-2));
s.push(opt);
s.call(1, Some(0))?;
}
}
}
Ok(())
}
fn parse_jitlib_cmd(cmd: &str) -> Option<(&str, &str)> {
match cmd {
"p" => Some(("p", "Flspv10")), // default -jp flags
"v" => Some(("v", "-")), // default -jv flags
"dump" => Some(("dump", "tirs")), // default -jdump flags
_ => cmd.split_once('='),
}
}
#[derive(Deref, DerefMut)]
pub struct Runtime {
#[deref]
#[deref_mut]
state: State,
cx: Context,
tasks: LocalSet,
}
task_local! {
static STATE: State;
}
impl Runtime {
pub fn spawn<T: 'static>(
&self,
f: impl AsyncFnOnce(&mut State) -> T + 'static,
f: impl AsyncFnOnce(&mut Context) -> T + 'static,
) -> JoinHandle<T> {
self.tasks
.spawn_local(async move { f(&mut STATE.with(|s| s.new_thread())).await })
.spawn_local(async move { f(&mut CURRENT.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>;
type IntoFuture = TaskLocalFuture<Context, LocalSet>;
fn into_future(self) -> Self::IntoFuture {
STATE.scope(self.state, self.tasks)
CURRENT.scope(self.cx, self.tasks)
}
}
task_local! {
static CURRENT: Context;
}
#[derive(Deref, DerefMut)]
pub struct Context {
#[deref]
#[deref_mut]
state: State,
report_err: Rc<ErrorFn>,
}
impl Context {
pub fn new_thread(&self) -> Self {
Self {
state: State::new_thread(&self.state),
report_err: self.report_err.clone(),
}
}
pub fn report_error(&self, err: &luajit::Error) {
(self.report_err)(&err);
}
}
pub fn spawn<T: 'static>(f: impl AsyncFnOnce(&mut Context) -> T + 'static) -> JoinHandle<T> {
// SAFETY: `new_thread` must be called inside `spawn_local` because this free-standing spawn
// function may be called via ffi from lua, and it is not safe to access the lua state within
// ffi calls.
spawn_local(async move { f(&mut CURRENT.with(|s| s.new_thread())).await })
}

3
crates/lb/src/task.lua Normal file
View File

@@ -0,0 +1,3 @@
local task = require("lb:task")
sleep = task.sleep
spawn = task.spawn

View File

@@ -1,46 +1,117 @@
//! # Task library
//!
//! The `lb:task` library primitives for asynchronous communication between tasks via message
//! passing channels.
//!
//! ## Exports
//!
//! See [`lb_tasklib`] for items exported by this library.
use crate::runtime::spawn;
use luaffi::{cdef, metatype};
use std::{ffi::c_int, process};
use tokio::task::JoinHandle;
use luaffi::{
cdef,
marker::{function, many},
metatype,
};
use std::{cell::RefCell, ffi::c_int, time::Duration};
use tokio::{task::JoinHandle, time::sleep};
#[cdef]
pub struct lb_libtask;
/// Items exported by the `lb:task` library.
///
/// This library can be acquired by calling
/// [`require("lb:task")`](https://www.lua.org/manual/5.1/manual.html#pdf-require).
///
/// ```lua
/// local task = require("lb:task");
/// ```
#[cdef(module = "lb:task")]
pub struct lb_tasklib;
#[metatype]
impl lb_libtask {
#[include("task.lua")]
impl lb_tasklib {
#[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!())))
pub async extern "Lua-C" fn sleep(ms: f64) {
sleep(Duration::from_secs_f64(ms.max(0.) / 1000.)).await;
}
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) };
pub extern "Lua" fn spawn(f: function, ...) -> lb_task {
// pack the function and its arguments into a table and pass its ref to rust.
//
// this "state" table is used from rust-side to call the function with its args, and it's
// also reused to store its return values that the task handle can return when awaited. the
// ref is owned by the task handle and unref'ed when it's gc'ed.
assert(
r#type(f) == "function",
concat!("function expected in argument 'f', got ", r#type(f)),
);
// we need two refs: one for the spawn call, and the other for the task handle. this is to
// ensure the task handle isn't gc'ed and the state table unref'ed before the spawn callback
// runs and puts the state table on the stack.
let state = __tpack(f, variadic!());
Self::__spawn(__ref(state), __ref(state))
}
extern "Lua-C" fn __spawn(spawn_ref: c_int, handle_ref: c_int) -> lb_task {
let handle = spawn(async move |cx| {
// SAFETY: handle_ref is always unique, created in Self::spawn above.
let state = unsafe { luajit::Ref::from_raw(cx, spawn_ref) };
let mut s = cx.guard();
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)
s.push(state); // this drops the state table ref, but the table is still on the stack
let narg = s.unpack(1, 1, None) - 1; // unpack the function and its args from the state table
match s.call_async(narg, None).await {
Ok(nret) => {
s.pack(1, nret); // pack the return values back into the state table
}
Err(err) => {
drop(s);
cx.report_error(&err);
}
}
println!("{s:?}");
});
lb_task { handle }
// spawn_ref is owned by the task handle and unref'ed there when the handle gets gc'ed
lb_task::new(handle, handle_ref)
}
}
/// Handle for an asynchronous task created by [`spawn`](lb_tasklib::spawn).
#[cdef]
pub struct lb_task {
#[opaque]
handle: JoinHandle<()>,
handle: RefCell<Option<JoinHandle<()>>>,
__ref: c_int,
}
#[metatype]
impl lb_task {}
impl lb_task {
fn new(handle: JoinHandle<()>, ref_key: c_int) -> Self {
lb_task {
handle: RefCell::new(Some(handle)),
__ref: ref_key,
}
}
pub async extern "Lua" fn r#await(&self) -> many {
self.__await();
let ret = __registry[self.__ref];
__tunpack(ret, 1, ret.n)
}
async extern "Lua-C" fn __await(&self) {
if let Some(handle) = self.handle.borrow_mut().take() {
handle
.await // task handler should never panic
.unwrap_or_else(|err| std::panic::resume_unwind(err.into_panic()));
}
}
#[gc]
extern "Lua" fn gc(&self) {
__unref(self.__ref);
}
}

30
crates/lb/src/time.rs Normal file
View File

@@ -0,0 +1,30 @@
use luaffi::{cdef, metatype};
#[cdef(module = "lb:time")]
pub struct lb_timelib;
#[metatype]
impl lb_timelib {
#[new]
extern "Lua-C" fn new() -> Self {
Self
}
pub extern "Lua-C" fn instant() -> lb_instant {
lb_instant::new(std::time::Instant::now())
}
}
#[cdef]
pub struct lb_instant(#[opaque] std::time::Instant);
#[metatype]
impl lb_instant {
fn new(instant: std::time::Instant) -> Self {
Self(instant)
}
pub extern "Lua-C" fn elapsed(&self) -> f64 {
self.0.elapsed().as_secs_f64()
}
}

17
crates/lb/tests/fs.lua Normal file
View File

@@ -0,0 +1,17 @@
local ok, fs = pcall(require, "lb:fs")
if not ok then return end
describe("temp files", function()
test("temp_dir cleans itself", function()
local path
do
local dir = fs.temp_dir()
path = dir:path()
assert(path and path ~= "")
fs.write(path .. "/test.txt", "test file")
assert(fs.read(path .. "/test.txt") == "test file")
end
collectgarbage()
assert(not pcall(fs.read, path .. "/test.txt"))
end)
end)

188
crates/lb/tests/net.lua Normal file
View File

@@ -0,0 +1,188 @@
local ok, net = pcall(require, "lb:net")
if not ok then return end
describe("ipaddr", function()
test("invalid ipaddr throws", function()
assert(not pcall(net.ipaddr, "invalid ip"))
end)
test("comparison", function()
local a = net.ipaddr("10.0.0.1")
local b = net.ipaddr("10.0.0.1")
local c = net.ipaddr("10.0.0.2")
assert(a ~= nil and a ~= {} and a ~= "10.0.0.1" and a ~= 167772161)
assert(a == a and a == b and a ~= c and b ~= c and c == c and c ~= a)
assert(a <= b and a < c and a <= c and b < c and b <= c and a <= a and c <= c)
assert(not (a < b or a > b or a > c or b > c or a >= c or b >= c))
end)
test("tostring", function()
local ip = net.ipaddr("10.0.0.1")
assert(tostring(ip) == "10.0.0.1")
end)
end)
describe("tcp", function()
describe("socket", function()
test("bind", function()
local socket = net.bind_tcp("127.0.0.1")
-- binds to the correct port
assert(tostring(socket:local_addr():ip()) == "127.0.0.1")
assert(socket:local_addr():port() ~= 0)
-- should not be able to rebind socket
assert(not pcall(socket.bind, socket, net.socketaddr("127.0.0.1")))
end)
test("options", function()
local socket = net.tcp()
-- keepalive
socket:set_keepalive(true)
assert(socket:keepalive() == true)
socket:set_keepalive(false)
assert(socket:keepalive() == false)
-- reuseaddr
socket:set_reuseaddr(true)
assert(socket:reuseaddr() == true)
socket:set_reuseaddr(false)
assert(socket:reuseaddr() == false)
-- reuseport not always supported on all platforms
-- sendbuf
socket:set_sendbuf(4096)
assert(socket:sendbuf() >= 4096)
assert(not pcall(socket.set_sendbuf, socket, 0))
assert(not pcall(socket.set_sendbuf, socket, -1))
-- recvbuf
socket:set_recvbuf(4096)
assert(socket:recvbuf() >= 4096)
assert(not pcall(socket.set_recvbuf, socket, 0))
assert(not pcall(socket.set_recvbuf, socket, -1))
-- linger
socket:set_linger(0)
assert(socket:linger() == 0)
socket:set_linger(2)
assert(math.abs(socket:linger() - 2) < 0.1)
socket:set_linger(-1)
assert(socket:linger() == 0)
-- nodelay
socket:set_nodelay(true)
assert(socket:nodelay() == true)
socket:set_nodelay(false)
assert(socket:nodelay() == false)
end)
test("can't use socket after conversion", function()
local socket = net.tcp()
socket:bind(net.socketaddr("127.0.0.1"))
socket:listen(10) -- convert to listener
assert(not pcall(socket.listen, socket, 10)) -- socket consumed
assert(not pcall(socket.local_addr, socket))
end)
end)
describe("stream", function()
test("no concurrent two reads/writes", function()
local listener = net.listen_tcp(net.localhost())
local client = net.connect_tcp(listener:local_addr())
local server = listener()
local reader = spawn(function()
assert(client:read(1) == nil) -- this should block first, then return nil from disconnection
end)
spawn(function()
assert(not pcall(client.read, client, 1)) -- this should fail, since the first task is still reading
end):await()
server:shutdown()
reader:await()
end)
test("allow concurrent read/write", function()
local listener = net.listen_tcp(net.localhost())
local client = net.connect_tcp(listener:local_addr())
local server = listener()
local reader = spawn(function()
assert(client:read(1) == nil) -- this should block first, then return nil from disconnection
end)
spawn(function()
client:write("hello") -- should be able to write while the first task is reading
end):await()
server:shutdown()
reader:await()
end)
test("stop reading from disconnected stream", function()
local listener = net.listen_tcp(net.localhost())
local client = net.connect_tcp(listener:local_addr())
local server = listener()
local reader = spawn(function()
while client:read(4) ~= nil do
end
assert(client:try_read(4) == nil)
assert(client:read_partial(4) == nil)
assert(client:read(4) == nil)
end)
for _ = 1, 10 do
assert(server:write("ping") == true)
end
sleep(100)
server:shutdown()
server = nil
collectgarbage()
reader:await()
end)
test("stop writing to disconnected stream", function()
local listener = net.listen_tcp(net.localhost())
local client = net.connect_tcp(listener:local_addr())
local server = listener()
local writer = spawn(function()
while client:write("pong") do
end
assert(client:try_write("pong") == nil)
assert(client:write_partial("pong") == nil)
assert(client:write("pong") == false)
end)
for _ = 1, 10 do
assert(server:read(4) == "pong")
end
sleep(100)
server:shutdown()
server = nil
collectgarbage()
writer:await()
end)
end)
describe("listener", function()
test("accept", function()
local listener = net.listen_tcp(net.localhost())
local addr = listener:local_addr()
local accepted = false
local client = net.tcp()
local accepted_stream
listener:on_accept(function(stream)
accepted = true
accepted_stream = stream
-- configure stream
stream:set_nodelay(true)
assert(stream:nodelay() == true)
end)
-- connect client
local client_stream = client:connect(addr)
local server_stream = listener()
assert(accepted)
assert(accepted_stream ~= nil)
-- check addresses
assert(server_stream:local_addr() ~= nil)
assert(server_stream:peer_addr() ~= nil)
assert(client_stream:local_addr() ~= nil)
assert(client_stream:peer_addr() ~= nil)
-- test data transfer
server_stream:write("hello")
local buf = client_stream:read(5)
assert(buf ~= nil and #buf == 5)
assert(buf == "hello")
-- shutdown
server_stream:shutdown()
client_stream:shutdown()
end)
end)
end)

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
}

189
crates/lb/tests/task.lua Normal file
View File

@@ -0,0 +1,189 @@
local ok, task = pcall(require, "lb:task")
if not ok then return end
describe("spawn", function()
test("callback receives args", function()
spawn(function(...)
assert(select("#", ...) == 0)
end):await()
spawn(function(...)
assert(select("#", ...) == 1)
assert((...) == nil)
end, nil):await()
spawn(function(...)
assert(select("#", ...) == 4)
local args = table.pack(...)
assert(args[1] == 1 and args[2] == 2 and args[3] == nil and args[4] == 3)
end, 1, 2, nil, 3):await()
end)
test("await returns callback results", function()
local res = table.pack(spawn(function()
-- no returns
end):await())
assert(res.n == 0)
local res = table.pack(spawn(function()
return nil
end):await())
assert(res.n == 1 and res[1] == nil)
local res = table.pack(spawn(function()
return 1, 2, nil, 3
end):await())
assert(res.n == 4 and res[1] == 1 and res[2] == 2 and res[3] == nil and res[4] == 3)
end)
test("handles invalid args", function()
assert(not pcall(spawn))
assert(not pcall(spawn, 123))
assert(not pcall(spawn, 1, 2, 3))
assert(not pcall(spawn, {}, 2, 3))
end)
test("callback args and results", function()
local res = table.pack(spawn(function(...)
assert(select("#", ...) == 5)
local args = table.pack(...)
assert(args[1] == 5 and args[2] == 4 and args[3] == nil and args[4] == 3, args[5] == nil)
return 1, 3, nil
end, 5, 4, nil, 3, nil):await())
assert(res.n == 3 and res[1] == 1 and res[2] == 3 and res[3] == nil)
end)
test("large number of args", function()
local args = {}
for i = 1, 1000 do
args[i] = i
end
local res = table.pack(spawn(function(...)
return ...
end, table.unpack(args)):await())
assert(res.n == 1000 and res[1] == 1 and res[1000] == 1000)
end)
test("callback closes over upvalues", function()
local x = 42
local function f()
return x
end
assert(spawn(f):await() == 42)
end)
test("order is consistent", function()
-- all tasks spawned in one batch should be resumed in the spawn order
local tasks, nums = {}, {}
for i = 1, 10 do
table.insert(
tasks,
spawn(function()
table.insert(nums, i)
end)
)
end
for i = 10, 1, -1 do
tasks[i]:await()
end
assert(#nums == 10)
for i = 1, 10 do
assert(nums[i] == i)
end
end)
test("nested spawns", function()
local result = {}
local function inner()
table.insert(result, "inner")
return "done"
end
local function outer()
table.insert(result, "outer")
local v = spawn(inner):await()
table.insert(result, v)
return v
end
local v = spawn(outer):await()
assert(v == "done")
assert(result[1] == "outer" and result[2] == "inner" and result[3] == "done")
end)
end)
describe("sleep", function()
test("invalid arg", function()
assert(not pcall(task.sleep, "invalid"))
task.sleep(-1) -- negative sleep should just become 0ms
end)
test("sleep is asynchronous", function()
local value
spawn(function()
value = "value"
end)
assert(value == nil)
task.sleep(100) -- implicit await: if it's synchronous, value wouldn't change
assert(value == "value")
end)
test("sleep in nested spawns", function()
local value1, value2, value3 = nil, nil, nil
local results = {}
local function inner()
task.sleep(30)
value1 = "set by inner"
table.insert(results, "inner")
return value1
end
local function middle()
task.sleep(20)
value2 = "set by middle"
local v = spawn(inner):await()
table.insert(results, v)
table.insert(results, "middle")
return v, value2
end
local function outer()
task.sleep(10)
value3 = "set by outer"
local v1, v2 = spawn(middle):await()
table.insert(results, v1)
table.insert(results, v2)
table.insert(results, "outer")
return v1, v2, value3
end
assert(value1 == nil and value2 == nil and value3 == nil)
local r1, r2, r3 = spawn(outer):await()
assert(r1 == "set by inner" and r2 == "set by middle" and r3 == "set by outer")
assert(value1 == "set by inner" and value2 == "set by middle" and value3 == "set by outer")
assert(results[1] == "inner")
assert(results[2] == "set by inner")
assert(results[3] == "middle")
assert(results[4] == "set by inner")
assert(results[5] == "set by middle")
assert(results[6] == "outer")
end)
end)
describe("task", function()
test("properly unrefs arg and ret table", function()
local registry = debug.getregistry()
local ref, t
local cb = function()
return "my ret"
end
do
local task = spawn(cb, "my arg")
ref = task.__ref
t = registry[ref]
assert(type(t) == "table")
assert(t[1] == cb)
assert(t[2] == "my arg")
task:await()
assert(registry[task.__ref] == t)
assert(t[1] == "my ret")
end
collectgarbage()
assert(registry[ref] ~= t) -- if task unref'ed it, it should be either nil or the next freelist number
end)
end)

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

@@ -1,11 +1,19 @@
[package]
name = "luaffi"
version = "0.1.0"
edition = "2024"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
[features]
option_ref_abi = []
option_string_abi = []
[dependencies]
bstr = "1.12.0"
luaffi_impl = { version = "0.1.0", path = "../luaffi_impl" }
luaify = { version = "0.1.0", path = "../luaify" }
luaffi_impl = { path = "../luaffi_impl" }
luaify = { path = "../luaify" }
rustc-hash = "2.1.1"
simdutf8 = "0.1.5"

View File

@@ -1,7 +1,7 @@
use crate::{
__internal::{display, type_id},
Cdef, CdefBuilder, FfiReturnConvention, IntoFfi, Metatype, MetatypeBuilder, Type, TypeBuilder,
TypeType, UnsafeExternCFn,
Cdef, CdefBuilder, ExternCFn, FfiReturnConvention, IntoFfi, Metatype, MetatypeBuilder, Type,
TypeBuilder, TypeType,
};
use luaify::luaify;
use std::{
@@ -70,6 +70,45 @@ enum State<F: Future> {
Complete,
}
unsafe impl<F: Future<Output: IntoFfi> + '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())
}
fn build(s: &mut TypeBuilder) {
s.cdef::<Self>().metatype::<Self>();
}
}
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::<ExternCFn<(&mut Self,), <F::Output as IntoFfi>::Into>>("__take")
.field::<ExternCFn<(&mut Self,), ()>>("__drop");
}
}
unsafe impl<F: Future<Output: IntoFfi> + 'static> Metatype for lua_future<F> {
type Target = Self;
fn build(s: &mut MetatypeBuilder) {
s.metatable_raw(
"gc",
luaify!(|self| {
self.__drop();
}),
);
}
}
impl<F: Future<Output: IntoFfi>> lua_future<F> {
pub fn new(fut: F) -> Self {
Self {
@@ -117,7 +156,12 @@ 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.
// AFAIK this because there is no way to find the size of a cdata payload using the C API.
// unfortunately we have to trust that the user won't do that right now.
//
// the only "saving grace" is that the user should never be running untrusted code anyway,
// because this whole project is based on the ffi library which should never be exposed to
// untrusted code in the first place.
self.sig == SIGNATURE
}
}
@@ -131,40 +175,6 @@ impl Future for lua_pollable {
}
}
unsafe impl<F: Future<Output: IntoFfi> + '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())
}
fn build(s: &mut TypeBuilder) {
s.cdef::<Self>().metatype::<Self>();
}
}
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: IntoFfi> + 'static> Metatype for lua_future<F> {
type Target = Self;
fn build(s: &mut MetatypeBuilder) {
s.metatable_raw("gc", luaify!(|self| self.__drop()));
}
}
unsafe impl<F: Future<Output: IntoFfi> + 'static> IntoFfi for lua_future<F> {
type Into = lua_future<F>;
@@ -177,6 +187,15 @@ unsafe impl<F: Future<Output: IntoFfi> + 'static> IntoFfi for lua_future<F> {
self
}
fn require_owned() -> bool {
// future always requires full ownership of itself even if it's a "temporary", because we
// must yield a full cdata to the runtime not a cdata containing a pointer to the future. if
// this is set to false, postlude might receive a reference lua_future cdata instead of a
// full lua_future cdata, and the runtime might incorrectly read the pointer value as
// lua_future itself (it does not dereference it).
true
}
fn postlude(ret: &str) -> impl Display {
// When returning a future from Rust to Lua, yield it immediately to the runtime which will
// poll it to completion in the background, then take the fulfilled value once the thread

View File

@@ -6,23 +6,6 @@ use std::{
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);

View File

@@ -1,24 +1,22 @@
---@diagnostic disable
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]
local function __ref(value)
if rawequal(value, nil) then return LUA_REFNIL end
local ref = __registry[FREELIST_REF]
if ref ~= nil and ref ~= 0 then
t[FREELIST_REF] = t[ref]
__registry[FREELIST_REF] = __registry[ref]
else
ref = #t + 1
ref = rawlen(__registry) + 1
end
t[ref] = value
__registry[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
local function __unref(ref)
if ref > 0 then
__registry[ref] = __registry[FREELIST_REF]
__registry[FREELIST_REF] = ref
end
end

File diff suppressed because it is too large Load Diff

193
crates/luaffi/src/marker.rs Normal file
View File

@@ -0,0 +1,193 @@
#![allow(non_camel_case_types)]
use crate::{
__internal::{disp, display},
Annotate,
};
use std::{fmt::Display, marker::PhantomData};
enum Marker {}
pub struct any(Marker);
impl Annotate for any {
fn annotation() -> impl Display {
"any"
}
}
pub struct many(Marker);
impl Annotate for many {
fn annotation() -> impl Display {
"..."
}
}
pub struct nil(Marker);
impl Annotate for nil {
fn annotation() -> impl Display {
"nil"
}
}
pub struct lightuserdata(Marker);
impl Annotate for lightuserdata {
fn annotation() -> impl Display {
"lightuserdata"
}
}
pub struct table<K, V>(Marker, PhantomData<*mut [(K, V)]>);
impl<K, V> Annotate for table<K, V>
where
K: Annotate,
V: Annotate,
{
fn annotation() -> impl Display {
display!("table<{}, {}>", K::annotation(), V::annotation())
}
}
pub struct function(Marker);
impl Annotate for function {
fn annotation() -> impl Display {
"function"
}
}
pub struct fun<I, O>(Marker, PhantomData<fn(I) -> O>);
macro_rules! impl_fun {
(fn($($arg:ident),*)) => {
impl<$($arg,)*> Annotate for fun<($($arg,)*), ()>
where
$($arg: Annotate,)*
{
fn annotation() -> impl Display {
disp(|f| {
write!(f, "fun(")?;
let mut _n = 0;
$(if _n != 0 { write!(f, ", ")?; } write!(f, "{}", $arg::annotation())?; _n += 1;)*
write!(f, ")")
})
}
}
};
(fn($($arg:ident),*) -> $ret:ident) => {
impl<$($arg,)* $ret> Annotate for fun<($($arg,)*), $ret>
where
$($arg: Annotate,)*
$ret: Annotate,
{
fn annotation() -> impl Display {
disp(|f| {
write!(f, "fun(")?;
let mut _n = 0;
$(if _n != 0 { write!(f, ", ")?; } write!(f, "{}", $arg::annotation())?; _n += 1;)*
write!(f, "): {}", $ret::annotation())
})
}
}
impl_fun!(fn($($arg),*));
};
}
impl_fun!(fn() -> A);
impl_fun!(fn(A) -> B);
impl_fun!(fn(A, B) -> C);
impl_fun!(fn(A, B, C) -> D);
impl_fun!(fn(A, B, C, D) -> E);
impl_fun!(fn(A, B, C, D, E) -> F);
impl_fun!(fn(A, B, C, D, E, F) -> G);
impl_fun!(fn(A, B, C, D, E, F, G) -> H);
impl_fun!(fn(A, B, C, D, E, F, G, H) -> I);
impl_fun!(fn(A, B, C, D, E, F, G, H, I) -> J);
pub struct userdata(Marker);
impl Annotate for userdata {
fn annotation() -> impl Display {
"userdata"
}
}
pub struct thread(Marker);
impl Annotate for thread {
fn annotation() -> impl Display {
"thread"
}
}
pub struct cdata(Marker);
impl Annotate for cdata {
fn annotation() -> impl Display {
"cdata"
}
}
pub struct Either<T, U>(Marker, PhantomData<(T, U)>);
impl<T, U> Annotate for Either<T, U>
where
T: Annotate,
U: Annotate,
{
fn annotation() -> impl Display {
display!("({} | {})", T::annotation(), U::annotation())
}
}
pub struct OneOf<X>(Marker, PhantomData<X>);
macro_rules! impl_oneof {
($($ty:ident),+) => {
impl<$($ty),+> Annotate for OneOf<($($ty,)+)>
where
$($ty: Annotate),+
{
fn annotation() -> impl Display {
disp(|f| {
write!(f, "(")?;
let mut _n = 0;
$(if _n != 0 { write!(f, " | ")?; } write!(f, "{}", $ty::annotation())?; _n += 1;)*
write!(f, ")")
})
}
}
};
}
impl_oneof!(A, B);
impl_oneof!(A, B, C);
impl_oneof!(A, B, C, D);
impl_oneof!(A, B, C, D, E);
impl_oneof!(A, B, C, D, E, F);
impl_oneof!(A, B, C, D, E, F, G);
impl_oneof!(A, B, C, D, E, F, G, H);
impl_oneof!(A, B, C, D, E, F, G, H, I);
impl<T> Annotate for Option<T>
where
T: Annotate,
{
fn annotation() -> impl Display {
display!("{}?", T::annotation())
}
}
impl<T, E> Annotate for Result<T, E>
where
T: Annotate,
{
fn annotation() -> impl Display {
display!("{}", T::annotation())
}
}

View File

@@ -0,0 +1,89 @@
use crate::{
__internal::{disp, display, type_id},
Cdef, CdefBuilder, IntoFfi, KEEP_FN, Metatype, MetatypeBuilder, Type, TypeBuilder, TypeType,
};
use std::{ffi::c_int, fmt::Display};
#[repr(C)]
#[allow(non_camel_case_types)]
pub enum lua_option<T> {
None, // __tag = 0
Some(T), // __tag = 1
}
unsafe impl<T: Type + 'static> Type for lua_option<T> {
fn name() -> impl Display {
display!("__option_{:x}", type_id::<T>())
}
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>().metatype::<Self>();
}
}
unsafe impl<T: Type + 'static> Cdef for lua_option<T> {
fn build(b: &mut CdefBuilder) {
b.field::<c_int>("__tag");
(T::ty() != TypeType::Void).then(|| b.field::<T>("__value"));
}
}
unsafe impl<T: Type + 'static> Metatype for lua_option<T> {
type Target = Self;
fn build(_b: &mut MetatypeBuilder) {}
}
unsafe impl<T: IntoFfi<Into: 'static>> IntoFfi for Option<T> {
type Into = lua_option<T::Into>;
fn convert(self) -> Self::Into {
match self {
Some(value) => lua_option::Some(T::convert(value)),
None => lua_option::None,
}
}
fn require_owned() -> bool {
// lua_option is only used to transmit information about whether we have a value or not and
// is forgotten immediately after use, so there is no need for an owned option
false
}
fn postlude(ret: &str) -> impl Display {
disp(move |f| {
write!(f, "if {ret}.__tag ~= 0 then ")?;
match T::Into::ty() {
TypeType::Void => write!(f, "{ret} = nil; ")?, // for void options, we don't have a __value
TypeType::Primitive => {
// can always copy primitives to stack
write!(f, "{ret} = {ret}.__value; {}", T::postlude(ret))?;
}
TypeType::Aggregate => {
let ct = T::Into::name();
if T::require_owned() {
// inner value requires ownership; copy it into its own cdata and forget
// option.
write!(f, "{ret} = __new(__ct.{ct}, {ret}.__value); ")?;
write!(f, "{}", T::postlude(ret))?;
} else {
// inner value is a "temporary" like an option itself and doesn't require
// full ownership of itself. we just need to keep the option object alive
// until its postlude completes.
write!(f, "local {ret}_keep = {ret}; {ret} = {ret}.__value; ")?;
write!(f, "do {}end; ", T::postlude(ret))?;
write!(f, "__C.{KEEP_FN}({ret}_keep); ")?; // keep original option alive
}
}
}
write!(f, "else {ret} = nil; end; ")
})
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
__internal::{disp, display},
Cdef, CdefBuilder, IntoFfi, Type, TypeBuilder, TypeType,
__internal::{disp, display, type_id},
Cdef, CdefBuilder, IntoFfi, KEEP_FN, Metatype, MetatypeBuilder, Type, TypeBuilder, TypeType,
string::{DROP_BUFFER_FN, lua_buffer},
};
use std::{ffi::c_int, fmt::Display};
@@ -12,9 +12,9 @@ pub enum lua_result<T> {
Ok(T), // __tag = 1
}
unsafe impl<T: Type> Type for lua_result<T> {
unsafe impl<T: Type + 'static> Type for lua_result<T> {
fn name() -> impl Display {
display!("result__{}", T::name())
display!("__result_{:x}", type_id::<T>())
}
fn ty() -> TypeType {
@@ -26,11 +26,11 @@ unsafe impl<T: Type> Type for lua_result<T> {
}
fn build(b: &mut TypeBuilder) {
b.cdef::<Self>();
b.cdef::<Self>().metatype::<Self>();
}
}
unsafe impl<T: Type> Cdef for lua_result<T> {
unsafe impl<T: Type + 'static> 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"));
@@ -39,7 +39,12 @@ unsafe impl<T: Type> Cdef for lua_result<T> {
}
}
unsafe impl<T: IntoFfi, E: Display> IntoFfi for Result<T, E> {
unsafe impl<T: Type + 'static> Metatype for lua_result<T> {
type Target = Self;
fn build(_b: &mut MetatypeBuilder) {}
}
unsafe impl<T: IntoFfi<Into: 'static>, E: Display> IntoFfi for Result<T, E> {
type Into = lua_result<T::Into>;
fn convert(self) -> Self::Into {
@@ -49,22 +54,44 @@ unsafe impl<T: IntoFfi, E: Display> IntoFfi for Result<T, E> {
}
}
fn require_owned() -> bool {
// lua_result is only used to transmit information about whether an operation succeeded or
// not and is forgotten immediately after use, so there is no need for an owned result
false
}
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))?;
TypeType::Void => write!(f, "{ret} = nil; ")?, // for void results, we don't have a __value
TypeType::Primitive => {
// can always copy primitives to stack
write!(f, "{ret} = {ret}.__value; {}", T::postlude(ret))?;
}
TypeType::Aggregate => {
let ct = T::Into::name();
if T::require_owned() {
// inner value requires ownership; copy it into its own cdata and forget
// result.
write!(f, "{ret} = __new(__ct.{ct}, {ret}.__value); ")?;
write!(f, "{}", T::postlude(ret))?;
} else {
// inner value is a "temporary" like a result itself and doesn't require
// full ownership of itself. we just need to keep the result object alive
// until its postlude completes.
write!(f, "local {ret}_keep = {ret}; {ret} = {ret}.__value; ")?;
write!(f, "do {}end; ", T::postlude(ret))?;
write!(f, "__C.{KEEP_FN}({ret}_keep); ")?; // keep original result alive
}
}
}
write!(
f,
"else \
local __{ret}_msg = __intern({ret}.__err.__ptr, {ret}.__err.__len); \
local {ret}_err = __intern({ret}.__err.__ptr, {ret}.__err.__len); \
__C.{DROP_BUFFER_FN}({ret}.__err); \
return error(__{ret}_msg); \
return error({ret}_err); \
end; "
)
})

View File

@@ -4,18 +4,18 @@ use crate::{
};
use bstr::{BStr, BString};
use luaffi_impl::{cdef, metatype};
use std::{fmt::Display, mem::ManuallyDrop, ptr, slice};
use std::{fmt::Display, mem::ManuallyDrop, slice};
pub(crate) const IS_UTF8_FN: &str = "luaffi_is_utf8";
pub(crate) const DROP_BUFFER_FN: &str = "luaffi_drop_buffer";
pub(crate) const IS_UTF8_FN: &str = "__lf_is_utf8";
pub(crate) const DROP_BUFFER_FN: &str = "__lf_drop_buffer";
#[unsafe(export_name = "luaffi_is_utf8")]
#[unsafe(export_name = "__lf_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(export_name = "__lf_drop_buffer")]
unsafe extern "C" fn __drop_buffer(buf: *mut lua_buffer) {
debug_assert!(!buf.is_null());
debug_assert!(!unsafe { (*buf).__ptr.is_null() });
@@ -42,6 +42,7 @@ impl lua_buf {
}
}
#[cfg(feature = "option_string_abi")]
pub(crate) fn null() -> Self {
Self {
__ptr: ptr::null(),
@@ -71,6 +72,7 @@ impl lua_buffer {
}
}
#[cfg(feature = "option_string_abi")]
pub(crate) fn null() -> Self {
Self {
__ptr: ptr::null_mut(),
@@ -154,8 +156,14 @@ unsafe impl IntoFfi for &'static [u8] {
lua_buf::new(self)
}
fn require_owned() -> bool {
// lua_buf is only used to have its contents interned then forgotten immediately; no need
// for ownership of it
false
}
fn postlude(ret: &str) -> impl Display {
display!("{ret} = __intern({ret}.__ptr, {ret}.__len)")
display!("{ret} = __intern({ret}.__ptr, {ret}.__len); ")
}
}
@@ -166,9 +174,15 @@ unsafe impl IntoFfi for Vec<u8> {
lua_buffer::new(self)
}
fn require_owned() -> bool {
// lua_buffer is only used to have its contents interned then forgotten immediately; no need
// for ownership of it
false
}
fn postlude(ret: &str) -> impl Display {
display!(
"do local __{ret} = {ret}; {ret} = __intern({ret}.__ptr, {ret}.__len); __C.{DROP_BUFFER_FN}(__{ret}); end; "
"local {ret}_buf = {ret}; {ret} = __intern({ret}.__ptr, {ret}.__len); __C.{DROP_BUFFER_FN}({ret}_buf); "
)
}
}
@@ -216,56 +230,64 @@ 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;
// `Option<String>: From/IntoFfi` isn't implemented because it conflicts with the generic
// `Option<T>: From/IntoFfi` impl and rust doesn't have specialisation yet (and probably not anytime
// soon). this is fine for now because we have specialisation for string-like parameters implemented
// in the #[metatype] macro already, and string returns wrapped in `Option<T>` isn't much additional
// overhead.
#[cfg(feature = "option_string_abi")]
mod impl_option_string {
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 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 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)))
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());
}
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

@@ -1,7 +1,11 @@
[package]
name = "luaffi_impl"
version = "0.1.0"
edition = "2024"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
[lib]
proc-macro = true

View File

@@ -5,18 +5,26 @@ use quote::{format_ident, quote, quote_spanned};
use syn::{ext::IdentExt, spanned::Spanned, *};
#[derive(Debug, FromMeta)]
pub struct Args {}
pub struct Args {
module: Option<String>,
}
pub fn transform(_args: Args, mut item: Item) -> Result<TokenStream> {
let (name, impl_type, impl_cdef) = match item {
pub fn transform(args: Args, mut item: Item) -> Result<TokenStream> {
let (name, impl_type, impl_module, impl_cdef) = match item {
Item::Struct(ref mut str) => (
str.ident.clone(),
generate_type(&str.ident)?,
args.module
.map(|name| generate_module(&name, &str.ident))
.transpose()?,
generate_cdef_structure(str)?,
),
Item::Enum(ref mut enu) => (
enu.ident.clone(),
generate_type(&enu.ident)?,
args.module
.map(|name| generate_module(&name, &enu.ident))
.transpose()?,
generate_cdef_enum(enu)?,
),
_ => syn_error!(item, "expected struct or enum"),
@@ -35,6 +43,7 @@ pub fn transform(_args: Args, mut item: Item) -> Result<TokenStream> {
mod #mod_name {
use super::*;
#impl_type
#impl_module
#impl_cdef
}
))
@@ -42,14 +51,11 @@ pub fn transform(_args: Args, mut item: Item) -> Result<TokenStream> {
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 name = ty.unraw().to_string();
Ok(quote_spanned!(span =>
Ok(quote!(
unsafe impl #ffi::Type for #ty {
fn name() -> impl ::std::fmt::Display {
#name
}
fn name() -> impl ::std::fmt::Display { #name }
fn ty() -> #ffi::TypeType {
#ffi::TypeType::Aggregate
@@ -64,6 +70,10 @@ fn generate_type(ty: &Ident) -> Result<TokenStream> {
}
}
impl #ffi::Annotate for #ty {
fn annotation() -> impl ::std::fmt::Display { #name }
}
// SAFETY: we can always implement `IntoFfi` because it transfers ownership from Rust to Lua
unsafe impl #ffi::IntoFfi for #ty {
type Into = Self;
@@ -72,6 +82,16 @@ fn generate_type(ty: &Ident) -> Result<TokenStream> {
))
}
fn generate_module(name: &str, ty: &Ident) -> Result<TokenStream> {
let ffi = ffi_crate();
Ok(quote!(
impl #ffi::Module for #ty {
fn name() -> impl ::std::fmt::Display { #name }
}
))
}
fn generate_cdef_structure(str: &mut ItemStruct) -> Result<TokenStream> {
syn_assert!(
str.generics.params.is_empty(),
@@ -81,10 +101,9 @@ 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_cdef_build(&parse_cfields(&mut str.fields)?)?;
Ok(quote_spanned!(span =>
Ok(quote!(
unsafe impl #ffi::Cdef for #ty {
fn build(b: &mut #ffi::CdefBuilder) { #build }
}
@@ -100,18 +119,16 @@ 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_cdef_build(&parse_cfields(&mut variant.fields)?)?;
Ok(quote!(b.inner_struct(|b| { #build })))
})
.collect::<Result<Vec<_>>>()?;
Ok(quote_spanned!(span =>
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;)* });
@@ -131,7 +148,7 @@ struct CFieldAttrs {
opaque: bool,
}
fn get_cfields(fields: &mut Fields) -> Result<Vec<CField>> {
fn parse_cfields(fields: &mut Fields) -> Result<Vec<CField>> {
match fields {
Fields::Named(fields) => fields.named.iter_mut(),
Fields::Unnamed(fields) => fields.unnamed.iter_mut(),
@@ -155,12 +172,11 @@ fn parse_cfield_attrs(attrs: &mut Vec<Attribute>) -> Result<CFieldAttrs> {
let mut parsed = CFieldAttrs::default();
let mut i = 0;
while let Some(attr) = attrs.get(i) {
if let Some(name) = attr.path().get_ident() {
if name == "opaque" {
parsed.opaque = true;
attrs.remove(i);
continue;
}
let path = attr.path();
if path.is_ident("opaque") {
parsed.opaque = true;
attrs.remove(i);
continue;
}
i += 1;
}
@@ -185,7 +201,7 @@ fn generate_cdef_build(fields: &[CField]) -> Result<TokenStream> {
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>());

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
use std::env;
use syn::{spanned::Spanned, *};
use syn::{ext::IdentExt, spanned::Spanned, *};
macro_rules! syn_error {
($src:expr, $($fmt:expr),+) => {{
@@ -57,13 +57,13 @@ pub fn is_unit(ty: &Type) -> bool {
pub fn is_primitivelike(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::Tuple(tuple) if tuple.elems.is_empty() => return true, // unit type
Type::Reference(_) | Type::Ptr(_) => return true,
Type::Paren(paren) => return is_primitivelike(&paren.elem),
Type::Path(path) => {
if let Some(name) = path.path.get_ident() {
matches!(
format!("{name}").as_str(),
return matches!(
name.unraw().to_string().as_str(),
"bool"
| "u8"
| "u16"
@@ -94,11 +94,88 @@ pub fn is_primitivelike(ty: &Type) -> bool {
| "c_size_t"
| "c_ssize_t"
| "c_ptrdiff_t"
)
} else {
false
);
}
}
_ => false,
_ => {}
}
false
}
#[derive(Debug, Clone, Copy)]
pub enum StringLike {
SliceU8,
Str,
BStr,
}
pub fn is_stringlike(ty: &Type) -> Option<StringLike> {
if let Type::Reference(ty) = ty
&& ty.mutability.is_none()
&& ty.lifetime.is_none()
{
Some(match *ty.elem {
Type::Slice(ref slice) => {
// match &[u8]
if let Type::Path(ref path) = *slice.elem
&& let Some(name) = path.path.get_ident()
{
match name.unraw().to_string().as_str() {
"u8" => StringLike::SliceU8,
_ => return None,
}
} else {
return None;
}
}
Type::Path(ref path) => {
// match &str or &BStr
if let Some(name) = path.path.get_ident() {
match name.unraw().to_string().as_str() {
"str" => StringLike::Str,
"BStr" => StringLike::BStr,
_ => return None,
}
} else {
return None;
}
}
_ => return None,
})
} else {
None
}
}
pub fn is_optionlike(ty: &Type) -> Option<&Type> {
if let Type::Path(path) = ty
&& path.path.leading_colon.is_none()
&& path.path.segments.len() == 1
&& let Some(segment) = path.path.segments.get(0)
&& segment.ident == "Option"
&& let PathArguments::AngleBracketed(ref angle) = segment.arguments
&& angle.args.len() == 1
&& let Some(GenericArgument::Type(ty)) = angle.args.get(0)
{
Some(ty)
} else {
None
}
}
pub fn is_resultlike(ty: &Type) -> Option<&Type> {
if let Type::Path(path) = ty
&& path.path.leading_colon.is_none()
&& path.path.segments.len() == 1
&& let Some(segment) = path.path.segments.get(0)
&& segment.ident == "Result"
&& let PathArguments::AngleBracketed(ref angle) = segment.arguments
&& angle.args.len() <= 2 // allow Result<T, E> or Result<T>
&& let Some(GenericArgument::Type(ty)) = angle.args.get(0)
{
Some(ty)
} else {
None
}
}

View File

@@ -1,7 +1,11 @@
[package]
name = "luaify"
version = "0.1.0"
edition = "2024"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
[lib]
proc-macro = true
@@ -10,3 +14,6 @@ proc-macro = true
proc-macro2 = "1.0.95"
quote = "1.0.40"
syn = { version = "2.0.103", features = ["full", "visit-mut"] }
[dev-dependencies]
pretty_assertions = "1.4.1"

View File

@@ -14,6 +14,12 @@ pub fn generate(expr: &Expr) -> Result<TokenStream> {
Ok(f.done())
}
pub fn generate_chunk(block: &Block) -> Result<TokenStream> {
let mut f = Formatter::default();
generate_block(&mut f, &block, Context::stmt(false))?;
Ok(f.done())
}
#[derive(Default)]
struct Formatter {
buf: String,
@@ -910,7 +916,7 @@ fn generate_pat_typed(f: &mut Formatter, typed: &PatType, cx: PatContext) -> Res
assert_no_attrs!(typed);
match *typed.ty {
Type::Infer(_) => generate_pat(f, &typed.pat, cx),
ref ty => syn_error!(ty, "cannot have type"),
ref ty => syn_error!(ty, "cannot specify type"),
}
}

View File

@@ -1,4 +1,10 @@
use crate::{generate::generate, transform::transform};
//! # luaify
//!
//! A Rust for generating Lua code from Rust syntax.
use crate::{
generate::{generate, generate_chunk},
transform::{transform, transform_chunk},
};
use proc_macro::TokenStream as TokenStream1;
use quote::ToTokens;
use syn::parse_macro_input;
@@ -16,3 +22,13 @@ pub fn luaify(input: TokenStream1) -> TokenStream1 {
}
.into()
}
#[proc_macro]
pub fn luaify_chunk(input: TokenStream1) -> TokenStream1 {
let mut block = parse_macro_input!(input);
match transform_chunk(&mut block).and_then(|()| generate_chunk(&block)) {
Ok(s) => s,
Err(err) => err.into_compile_error().into_token_stream(),
}
.into()
}

View File

@@ -1,6 +1,4 @@
use crate::utils::{LuaType, expr_ident, pat_ident, syn_error, wrap_expr_block};
use quote::format_ident;
use std::mem;
use crate::utils::syn_error;
use syn::{spanned::*, visit_mut::*, *};
pub fn transform(expr: &mut Expr) -> Result<()> {
@@ -9,6 +7,12 @@ pub fn transform(expr: &mut Expr) -> Result<()> {
visitor.result
}
pub fn transform_chunk(block: &mut Block) -> Result<()> {
let mut visitor = Visitor::new();
visitor.visit_block_mut(block);
visitor.result
}
#[derive(Debug)]
struct Visitor {
result: Result<()>,
@@ -21,27 +25,6 @@ impl Visitor {
}
impl VisitMut for Visitor {
fn visit_expr_closure_mut(&mut self, clo: &mut ExprClosure) {
match self.transform_expr_closure(clo) {
res @ Err(_) => self.result = res,
_ => visit_expr_closure_mut(self, clo),
}
}
fn visit_item_fn_mut(&mut self, func: &mut ItemFn) {
match self.transform_function(func) {
res @ Err(_) => self.result = res,
_ => visit_item_fn_mut(self, func),
}
}
fn visit_expr_mut(&mut self, expr: &mut Expr) {
match self.transform_expr(expr) {
res @ Err(_) => self.result = res,
_ => visit_expr_mut(self, expr),
}
}
fn visit_expr_unary_mut(&mut self, un: &mut ExprUnary) {
match self.transform_unary(un) {
res @ Err(_) => self.result = res,
@@ -58,147 +41,9 @@ impl VisitMut for Visitor {
}
impl Visitor {
fn transform_expr_closure(&mut self, clo: &mut ExprClosure) -> Result<()> {
//
// transforms a closure expression with input type annotations by removing the annotations
// and inserting `as` casts at the start.
//
// before:
// |a: string, b: number| { ... }
// after:
// |a, b| { a as string; b as number; ... }
//
let mut checks: Vec<Stmt> = vec![];
for input in clo.inputs.iter_mut() {
match input {
Pat::Ident(_) => {}
Pat::Type(typed) => {
let ident = pat_ident(&typed.pat)?;
let ty = mem::replace(&mut typed.ty, parse_quote!(_));
match (&*ty).try_into()? {
LuaType::Any => {}
_ => checks.push(parse_quote! { #ident as #ty; }),
}
}
_ => {}
}
}
if !checks.is_empty() {
let mut body = wrap_expr_block(&clo.body);
body.stmts.splice(..0, checks);
clo.body = Box::new(parse_quote! { #body });
}
Ok(())
}
fn transform_function(&mut self, func: &mut ItemFn) -> Result<()> {
//
// transforms a function item with input type annotations by removing the annotations
// and inserting `as` casts at the start.
//
// before:
// fn my_func(self: table, a: string) { ... }
// after:
// fn my_func(self: _, a: _) { self as table; a as string; ... }
//
let mut checks: Vec<Stmt> = vec![];
for input in func.sig.inputs.iter_mut() {
if let Some((ident, ty)) = match input {
FnArg::Receiver(recv) if recv.colon_token.is_some() => {
let ty = mem::replace(&mut recv.ty, parse_quote!(_));
recv.colon_token = None;
Some((Ident::new("self", recv.self_token.span()), ty))
}
FnArg::Typed(typed) => {
let ident = pat_ident(&typed.pat)?;
let ty = mem::replace(&mut typed.ty, parse_quote!(_));
Some((ident, ty))
}
_ => None,
} {
match (&*ty).try_into()? {
LuaType::Any => {}
_ => checks.push(parse_quote! { #ident as #ty; }),
}
};
}
func.block.stmts.splice(..0, checks);
Ok(())
}
fn transform_expr(&mut self, expr: &mut Expr) -> Result<()> {
self.transform_expr_cast(expr)?;
Ok(())
}
fn transform_expr_cast(&mut self, expr: &mut Expr) -> Result<()> {
//
// transforms an `as` cast expression into a block expression containing a runtime
// lua type check.
//
// before:
// var as string
// after:
// { if type(var) != "string" { error(...) } }
//
if let Expr::Cast(cast) = expr {
let arg = (*cast.expr).clone();
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 = Ident::new("_", arg.span());
prelude = Some(parse_quote! { let #ident = #arg; });
(ident, format!("{ty} expected, got "))
}
};
let tmp = format_ident!("__{ident}");
let span = cast.span();
*expr = match ty {
LuaType::Any => parse_quote_spanned!(span => {}),
LuaType::Nil => parse_quote_spanned!(span => {
#prelude
assert(#ident == (), concat!(#msg, r#type(#ident)));
}),
LuaType::Number => parse_quote_spanned!(span => {
#prelude
let #tmp = #ident;
#ident = tonumber(#ident);
assert(#ident != (), concat!(#msg, r#type(#tmp)));
}),
LuaType::Integer => parse_quote_spanned!(span => {
#prelude
let #tmp = #ident;
#ident = tonumber(#ident);
assert(#ident != () && math::floor(#ident) == #ident, concat!(#msg, r#type(#tmp)));
}),
LuaType::String => parse_quote_spanned!(span => {
#prelude
if r#type(#ident) == "number" {
#ident = tostring(#ident);
} else {
assert(r#type(#ident) == "string", concat!(#msg, r#type(#ident)));
}
}),
_ => parse_quote_spanned!(span => {
#prelude
assert(r#type(#ident) == #ty_str, concat!(#msg, r#type(#ident)));
}),
}
}
Ok(())
}
fn transform_unary(&mut self, un: &mut ExprUnary) -> Result<()> {
//
// separates a nested negation unary operator with parentheses, because double hyphen
// separates a nested negation unary operator with parentheses, because the double hyphen
// `--` indicates a comment in lua.
//
// before:
@@ -210,7 +55,7 @@ impl Visitor {
&& let Expr::Unary(ref inner) = *un.expr
&& let UnOp::Neg(_) = inner.op
{
un.expr = Box::new(parse_quote!((#inner)));
un.expr = Box::new(parse_quote_spanned!(inner.span() => (#inner)));
}
Ok(())

View File

@@ -1,5 +1,4 @@
use std::fmt;
use syn::{ext::*, spanned::*, *};
use syn::*;
macro_rules! syn_error {
($src:expr, $($fmt:expr),+) => {{
@@ -24,90 +23,3 @@ pub fn wrap_expr_block(expr: &Expr) -> Block {
expr => parse_quote!({ #expr }),
}
}
pub fn 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> {
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()),
_ => syn_error!(pat, "expected ident"),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LuaType {
Any,
Nil,
Boolean,
Lightuserdata,
Number,
Integer,
String,
Table,
Function,
Userdata,
Thread,
Cdata,
}
impl fmt::Display for LuaType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LuaType::Any => write!(f, "any"),
LuaType::Nil => write!(f, "nil"),
LuaType::Boolean => write!(f, "boolean"),
LuaType::Lightuserdata => write!(f, "lightuserdata"),
LuaType::Number => write!(f, "number"),
LuaType::Integer => write!(f, "integer"),
LuaType::String => write!(f, "string"),
LuaType::Table => write!(f, "table"),
LuaType::Function => write!(f, "function"),
LuaType::Userdata => write!(f, "userdata"),
LuaType::Thread => write!(f, "thread"),
LuaType::Cdata => write!(f, "cdata"),
}
}
}
impl TryFrom<&Ident> for LuaType {
type Error = Error;
fn try_from(value: &Ident) -> Result<Self> {
Ok(match format!("{}", value.unraw()).as_str() {
"any" => Self::Any,
"nil" => Self::Nil,
"boolean" => Self::Boolean,
"lightuserdata" => Self::Lightuserdata,
"number" => Self::Number,
"integer" => Self::Integer,
"string" => Self::String,
"table" => Self::Table,
"function" => Self::Function,
"userdata" => Self::Userdata,
"thread" => Self::Thread,
"cdata" => Self::Cdata,
_ => syn_error!(value, "invalid lua type"),
})
}
}
impl TryFrom<&Type> for LuaType {
type Error = Error;
fn try_from(value: &Type) -> Result<Self> {
match value {
Type::Infer(_) => Ok(Self::Any),
Type::Path(path) if path.qself.is_none() => path.path.require_ident()?.try_into(),
_ => syn_error!(value, "invalid lua type"),
}
}
}

View File

@@ -1,4 +1,5 @@
use luaify::luaify;
use luaify::{luaify, luaify_chunk};
use pretty_assertions::assert_eq;
#[test]
fn raw_ident() {
@@ -76,13 +77,6 @@ fn local_fn() {
}),
r#"function(a,b)local function inner(c,d)end;return inner;end"#
);
assert_eq!(
luaify!(|| {
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"#
);
}
#[test]
@@ -207,37 +201,6 @@ fn loops() {
);
}
#[test]
fn type_checks() {
assert_eq!(luaify!(|s| {}), r#"function(s)end"#);
assert_eq!(
luaify!(|s: table| {}),
r#"function(s)do assert(type(s)=="table","table expected in \'s\', got "..type(s));end;end"#
);
assert_eq!(
luaify!(|s| { s as string }),
r#"function(s)do if type(s)=="number"then s=tostring(s);else assert(type(s)=="string","string expected in \'s\', got "..type(s));end;end;end"#
);
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"#
);
assert_eq!(
luaify!(|s| { s as nil }),
r#"function(s)do assert(s==nil,"nil expected in \'s\', got "..type(s));end;end"#
);
assert_eq!(luaify!(|s| { s as any }), r#"function(s)do end;end"#);
assert_eq!(
luaify!(|s| {
let (ok, res) = coroutine::r#yield(thread);
ok as boolean;
res as nil;
}),
r#"function(s)local ok,res=coroutine.yield(thread);do assert(type(ok)=="boolean","boolean expected in \'ok\', got "..type(ok));end;do assert(res==nil,"nil expected in \'res\', got "..type(res));end;end"#
);
}
#[test]
fn concat() {
assert_eq!(luaify!(concat!(a)), r#"a"#);
@@ -402,3 +365,19 @@ fn length() {
r#"local a,b,c=#a,#b,#c;"#
);
}
#[test]
fn chunk() {
assert_eq!(
luaify_chunk!({
if a == b {
c()
} else if b == c {
a()
} else {
d()
}
}),
"if a==b then c();elseif b==c then a();else d();end;"
);
}

View File

@@ -1,7 +1,11 @@
[package]
name = "luajit-sys"
version = "0.1.0"
edition = "2024"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
[lib]
path = "lib.rs"

View File

@@ -1,7 +1,11 @@
[package]
name = "luajit"
version = "0.1.0"
edition = "2024"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
[features]
runtime = ["luajit-sys/runtime"]
@@ -10,6 +14,5 @@ unwind = ["luajit-sys/unwind"]
[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"
luaffi = { path = "../luaffi" }
luajit-sys = { path = "../luajit-sys" }

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,24 @@
#[cfg(feature = "task")]
pub use lb::chan;
#[cfg(feature = "fs")]
pub use lb::fs;
#[cfg(feature = "net")]
pub use lb::net;
#[cfg(feature = "task")]
pub use lb::task;
#[cfg(feature = "time")]
pub use lb::time;
#[doc(hidden)]
pub fn open(#[allow(unused)] rt: &mut lb::runtime::Builder) {
#[cfg(feature = "task")]
rt.module::<task::lb_tasklib>();
#[cfg(feature = "task")]
rt.module::<chan::lb_chanlib>();
#[cfg(feature = "time")]
rt.module::<time::lb_timelib>();
#[cfg(feature = "fs")]
rt.module::<fs::lb_fslib>();
#[cfg(feature = "net")]
rt.module::<net::lb_netlib>();
}

View File

@@ -1,7 +1,8 @@
use clap::Parser;
use luajit::Chunk;
use mimalloc::MiMalloc;
use owo_colors::OwoColorize;
use std::{backtrace::Backtrace, fmt::Display, net::SocketAddr, num::NonZero, panic, thread};
use std::{backtrace::Backtrace, fmt::Display, num::NonZero, panic, process, thread};
use sysexits::ExitCode;
#[global_allocator]
@@ -20,21 +21,42 @@ fn panic_cb(panic: &panic::PanicHookInfo) {
};
eprint!(
"{}:\n{trace}",
"{}\n{trace}",
format_args!(
"thread '{}' panicked at {location}: {msg}",
thread::current().name().unwrap_or("<unnamed>")
)
.red()
.bold()
);
eprintln!(
"{}",
"This is a bug in luby. Please kindly report this at https://git.lua.re/luaneko/luby."
.yellow()
format_args!(
"luby should never panic. Please kindly report this bug at {}.",
env!("CARGO_PKG_REPOSITORY")
)
.yellow()
.bold()
);
}
fn error_cb(err: &luajit::Error) {
match err.trace() {
Some(trace) => eprintln!("{}\n{trace}", err.red().bold()),
None => eprintln!("{}", err.red().bold()),
}
process::exit(1);
}
fn unwrap_exit<T, E: Display>(code: ExitCode) -> impl FnOnce(E) -> T {
move |err| {
eprintln!("{}", err.red().bold());
code.exit()
}
}
#[derive(Debug, Parser)]
struct Args {
/// Paths to scripts to execute.
@@ -45,7 +67,7 @@ struct Args {
#[clap(long, short = 'e', value_name = "CHUNK")]
eval: Vec<String>,
/// Libraries to require on startup.
/// Libraries to require.
#[clap(long, short = 'l', value_name = "NAME")]
lib: Vec<String>,
@@ -54,29 +76,65 @@ struct Args {
log: tracing::Level,
/// LuaJIT control commands.
#[clap(long, short = 'j', value_name = "CMD=FLAGS")]
#[clap(long, short = 'j', help_heading = "Runtime", value_name = "CMD=FLAGS")]
jit: Vec<String>,
/// Number of tokio worker threads.
#[clap(long, value_name = "THREADS", default_value_t = Self::threads())]
/// Allow global variables.
#[clap(
long,
help_heading = "Runtime",
value_name = "ENABLED",
default_value_t = true
)]
allow_globals: bool,
/// Number of worker threads.
#[clap(
long,
short = 'T',
help_heading = "Runtime",
value_name = "COUNT",
default_value_t = Self::threads()
)]
threads: NonZero<usize>,
/// Number of tokio blocking threads.
#[clap(long, value_name = "THREADS", default_value_t = Self::blocking_threads())]
/// Number of blocking threads.
#[clap(
long,
help_heading = "Runtime",
value_name = "COUNT",
default_value_t = Self::blocking_threads()
)]
blocking_threads: NonZero<usize>,
/// Enable tokio-console integration.
#[clap(long)]
#[cfg(feature = "tokio-console")]
#[clap(long, help_heading = "Debugging", value_name = "ENABLED")]
enable_console: bool,
/// tokio-console publish address.
#[cfg(feature = "tokio-console")]
#[clap(
long,
help_heading = "Debugging",
value_name = "ADDRESS",
default_value = "127.0.0.1:6669",
requires = "enable_console"
)]
console_addr: SocketAddr,
console_addr: std::net::SocketAddr,
/// Dump internal data.
#[clap(
long,
help_heading = "Debugging",
value_name = "DATA",
value_parser = ["cdef"]
)]
dump: Vec<String>,
/// Print version.
#[clap(long, short = 'V')]
version: bool,
}
impl Args {
@@ -89,17 +147,15 @@ impl Args {
}
}
fn exit_err<T, E: Display>(code: ExitCode) -> impl FnOnce(E) -> T {
move |err| {
eprintln!("{}", err.red());
code.exit()
}
}
fn main() -> Result<(), ExitCode> {
fn main() -> ExitCode {
panic::set_hook(Box::new(panic_cb));
let args = Args::parse();
if args.version {
print_version();
return ExitCode::Ok;
}
init_logger(&args);
let tokio = init_tokio(&args);
@@ -108,13 +164,28 @@ fn main() -> Result<(), ExitCode> {
tokio.block_on(async {
lua.await;
main.await.unwrap()
match main.await {
Ok(res) => res,
Err(err) => panic::resume_unwind(err.into_panic()),
}
})
}
fn print_version() {
println!("luby {}", env!("VERGEN_GIT_DESCRIBE"));
println!("{}\n", env!("CARGO_PKG_HOMEPAGE"));
println!("Compiled with {} -- {}", luajit::version(), luajit::url());
println!(
"Compiled with rustc {} on {} for {}",
env!("VERGEN_RUSTC_SEMVER"),
env!("VERGEN_RUSTC_HOST_TRIPLE"),
env!("VERGEN_CARGO_TARGET_TRIPLE"),
);
}
fn init_logger(args: &Args) {
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{Layer, util::*};
use tracing_subscriber::util::*;
let log = tracing_subscriber::fmt()
.compact()
@@ -128,98 +199,68 @@ fn init_logger(args: &Args) {
.with_target(false)
.finish();
if args.enable_console {
#[cfg(feature = "tokio-console")]
{
use tracing_subscriber::Layer;
console_subscriber::ConsoleLayer::builder()
.with_default_env()
.server_addr(args.console_addr)
.spawn()
.with_subscriber(log)
.init()
} else {
log.init()
.init();
}
#[cfg(not(feature = "tokio-console"))]
log.init();
}
fn init_tokio(args: &Args) -> tokio::runtime::Runtime {
let mut rt = match args.threads.get() {
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))
}
.enable_all()
.thread_name("luby")
.max_blocking_threads(args.blocking_threads.get())
.build()
.unwrap_or_else(unwrap_exit(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));
let mut rt = lb::runtime::Builder::new();
luby::open(&mut rt);
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));
if args.dump.iter().find(|s| *s == "cdef").is_some() {
print!("{}", rt.registry()); // for cdef debugging
}
rt
rt.unhandled_error(error_cb)
.prohibit_globals(!args.allow_globals)
.jit_opts(args.jit.iter())
.build()
.unwrap()
}
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> {
async fn main_async(args: Args, cx: &mut lb::runtime::Context) -> 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();
eprintln!("{}", format_args!("{path}: {err}").red().bold());
return ExitCode::NoInput;
}
};
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();
if let Err(ref err) = cx.load(&Chunk::new(chunk).with_path(path)) {
cx.report_error(err);
} else if let Err(ref err) = cx.call_async(0, Some(0)).await {
cx.report_error(err);
}
}
Ok(())
ExitCode::Ok
}

View File

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

157
tests/main.lua Normal file
View File

@@ -0,0 +1,157 @@
if (...) ~= nil and (...).type == "group" then return end -- prevent recursive harness call
local ok = pcall(require, "lb:task")
if not ok then error("lua test harness requires 'lb:task'") end
local ok, time = pcall(require, "lb:time")
if not ok then error("lua test harness requires 'lb:time'") end
local ok, fs = pcall(require, "lb:fs")
if not ok then error("lua test harness requires 'lb:fs'") end
local global = _G
local color = {
reset = "\x1b[0m",
pass = "\x1b[32;1m", -- green
fail = "\x1b[31;1m", -- red
}
local icon = {
check = "\u{2713}",
cross = "\u{00d7}",
chevron = "\u{203a}",
}
local function style(name, s)
return ("%s%s%s"):format(color[name], s, color.reset)
end
local function create_test(name, f, group)
local test = { type = "test", name = name or "", group = group, state = "pending", f = f }
local fenv = setmetatable({}, { __index = global, __newindex = global })
setfenv(f, fenv)
return test
end
local function create_group(name, f, parent)
local group = { type = "group", name = name or "", parent = parent, items = {} }
local fenv = setmetatable({
describe = function(name, f)
local item = create_group(name, f, group)
table.insert(group.items, item)
return item
end,
test = function(name, f)
local item = create_test(name, f, group)
table.insert(group.items, item)
return item
end,
}, { __index = global, __newindex = global })
setfenv(f, fenv)
f(group)
return group
end
local function name_test(test)
local name = test.name
local group = test.group
while group ~= nil do
if group.name ~= "" then name = ("%s %s %s"):format(group.name, icon.chevron, name) end
group = group.parent
end
return name
end
local function trace(msg)
return style("fail", msg) .. debug.traceback("", 2):sub(("\nstack traceback:"):len() + 1)
end
local function run_test(test)
local ok, res = xpcall(test.f, trace, test)
if ok then
test.state = "pass"
print("", ("%s %s"):format(style("pass", "PASS"), name_test(test)))
else
test.state = "fail"
print("", ("%s %s\n\n%s\n"):format(style("fail", "FAIL"), name_test(test), res))
end
collectgarbage() -- gc after each test to test destructors
return test
end
local function start(cx, item)
if item.type == "test" then
table.insert(cx.tasks, spawn(run_test, item))
elseif item.type == "group" then
for _, item in ipairs(item.items) do
start(cx, item)
end
end
end
local function check_refs()
-- ensure all refs were properly unref'ed
local registry = debug.getregistry()
local count = #registry
local ref = 0 -- FREELIST_REF
while type(registry[ref]) == "number" do
local next = registry[ref]
registry[ref], ref = nil, next
end
for i = 1, count do
local value = registry[i]
if type(value) ~= "thread" then -- ignore threads pinned by the runtime
assert(rawequal(registry[i], nil), ("ref %d not unref'ed: %s"):format(i, registry[i]))
end
end
end
local function main(item)
local cx = { tasks = {} }
local time, pass, fail = time.instant(), 0, 0
start(cx, item)
for _, task in ipairs(cx.tasks) do
if task:await().state == "pass" then
pass = pass + 1
else
fail = fail + 1
end
end
local elapsed = time:elapsed()
local code = 1
if fail == 0 then
print("", style("pass", ("%s %d tests passed"):format(icon.check, pass)))
code = 0
else
print(
"",
("%s, %s"):format(
style("pass", ("%s %d tests passed"):format(icon.check, pass)),
style("fail", ("%s %d tests failed"):format(icon.cross, fail))
)
)
end
if elapsed < 1000 then
print("", ("%s completed in %.2f ms"):format(icon.chevron, elapsed * 1000))
else
print("", ("%s completed in %.2f s"):format(icon.chevron, elapsed))
end
cx = nil
collectgarbage()
check_refs()
return code -- report error to cargo
end
return main(create_group("", function()
local function include(path, pat)
for entry in fs.glob_dir(path, pat) do
local path = entry:path()
local f, err = loadfile(path)
if not f then error(err) end
describe(path, f)
end
end
include("tests", "**/*.lua")
include("crates", "*/tests/**/*.lua")
end))

50
tests/main.rs Normal file
View File

@@ -0,0 +1,50 @@
use luajit::Chunk;
use owo_colors::OwoColorize;
use std::{
fs, panic,
process::{self, ExitCode},
};
fn main() -> ExitCode {
let tokio = tokio::runtime::Runtime::new().unwrap();
let lua = {
let mut rt = lb::runtime::Builder::new();
luby::open(&mut rt);
rt.unhandled_error(error_cb)
.prohibit_globals(true)
.build()
.unwrap()
};
let path = "tests/main.lua";
let main = lua.spawn(async move |s| {
if let Err(ref err) = s.load(&Chunk::new(fs::read(path).unwrap()).with_path(path)) {
s.report_error(err);
} else if let Err(ref err) = s.call_async(0, Some(1)).await {
s.report_error(err);
}
if s.slot(1).integer().unwrap_or(1) == 0 {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
});
tokio.block_on(async move {
lua.await;
match main.await {
Ok(res) => res,
Err(err) => panic::resume_unwind(err.into_panic()),
}
})
}
fn error_cb(err: &luajit::Error) {
match err.trace() {
Some(trace) => eprintln!("{}\n{trace}", err.red().bold()),
None => eprintln!("{}", err.red().bold()),
}
process::exit(1);
}