Compare commits

..

88 Commits

Author SHA1 Message Date
da2598b534
Run all lua examples in docs as doctests 2025-06-30 22:01:00 +10:00
81cd901ea6
Fix up tests 2025-06-30 20:11:54 +10:00
f2a13df169
Don't use semicolons in lua examples 2025-06-30 20:07:58 +10:00
9d7e35094c
Restructure netlib 2025-06-30 20:07:13 +10:00
e03ef8a495
Restructure fslib 2025-06-30 19:23:44 +10:00
fc4d27abf1
Add sqlite stub library 2025-06-30 17:58:12 +10:00
3de17cb77e
Update docs 2025-06-30 17:22:12 +10:00
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
57 changed files with 5756 additions and 2236 deletions

View File

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

209
Cargo.lock generated
View File

@ -534,6 +534,12 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@ -573,6 +579,24 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.1.2" version = "1.1.2"
@ -589,6 +613,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
@ -685,6 +715,19 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 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]] [[package]]
name = "h2" name = "h2"
version = "0.4.10" version = "0.4.10"
@ -715,6 +758,18 @@ name = "hashbrown"
version = "0.15.4" version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
dependencies = [
"foldhash",
]
[[package]]
name = "hashlink"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.15.4",
]
[[package]] [[package]]
name = "hdrhistogram" name = "hdrhistogram"
@ -1025,14 +1080,25 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
name = "lb" name = "lb"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"camino",
"derive_more", "derive_more",
"globset",
"luaffi", "luaffi",
"luaify", "luaify",
"luajit", "luajit",
"sysexits", "sysexits",
"tempfile",
"thiserror",
"tokio", "tokio",
"tracing", "walkdir",
]
[[package]]
name = "lb_sqlite"
version = "0.0.1"
dependencies = [
"lb",
"luaffi",
"rusqlite",
] ]
[[package]] [[package]]
@ -1073,6 +1139,18 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "libsqlite3-sys"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91632f3b4fb6bd1d72aa3d78f41ffecfcf2b1a6648d8c241dbe7dbfaf4875e15"
dependencies = [
"bindgen",
"cc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "libz-sys" name = "libz-sys"
version = "1.1.22" version = "1.1.22"
@ -1097,16 +1175,6 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "lock_api"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.27" version = "0.4.27"
@ -1138,6 +1206,7 @@ dependencies = [
name = "luaify" name = "luaify"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"pretty_assertions",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@ -1151,7 +1220,6 @@ dependencies = [
"bstr", "bstr",
"luaffi", "luaffi",
"luajit-sys", "luajit-sys",
"thiserror",
] ]
[[package]] [[package]]
@ -1170,6 +1238,7 @@ dependencies = [
"clap", "clap",
"console-subscriber", "console-subscriber",
"lb", "lb",
"lb_sqlite",
"luajit", "luajit",
"mimalloc", "mimalloc",
"owo-colors", "owo-colors",
@ -1319,29 +1388,6 @@ version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
[[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",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -1410,6 +1456,16 @@ dependencies = [
"zerocopy", "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]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.35" version = "0.2.35"
@ -1506,15 +1562,6 @@ dependencies = [
"getrandom 0.2.16", "getrandom 0.2.16",
] ]
[[package]]
name = "redox_syscall"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@ -1559,6 +1606,20 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rusqlite"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3de23c3319433716cf134eed225fe9986bc24f63bed9be9f20c329029e672dc7"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.25" version = "0.1.25"
@ -1606,10 +1667,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]] [[package]]
name = "scopeguard" name = "same-file"
version = "1.2.0" version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "semver" name = "semver"
@ -1667,15 +1731,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 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]] [[package]]
name = "simdutf8" name = "simdutf8"
version = "0.1.5" version = "0.1.5"
@ -1750,6 +1805,19 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "198f60d1f7f003f168507691e42d082df109ef0f05c6fd006e22528371a5f1b4" 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]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.12" version = "2.0.12"
@ -1832,9 +1900,7 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"tracing", "tracing",
@ -2113,6 +2179,16 @@ dependencies = [
"rustversion", "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]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@ -2164,6 +2240,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 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]] [[package]]
name = "winapi-x86_64-pc-windows-gnu" name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
@ -2346,6 +2431,12 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.0" version = "0.8.0"

View File

@ -2,6 +2,7 @@
resolver = "3" resolver = "3"
members = [ members = [
"crates/lb", "crates/lb",
"crates/lb_sqlite",
"crates/luaffi", "crates/luaffi",
"crates/luaffi_impl", "crates/luaffi_impl",
"crates/luaify", "crates/luaify",
@ -30,17 +31,31 @@ repository.workspace = true
dev.panic = "abort" dev.panic = "abort"
release.panic = "abort" release.panic = "abort"
[[test]]
name = "main"
harness = false
[features]
default = ["task", "time", "fs", "net", "sqlite"]
task = ["lb/task"]
time = ["lb/time"]
fs = ["lb/fs"]
net = ["lb/net"]
sqlite = ["dep:lb_sqlite"]
tokio-console = ["dep:console-subscriber"]
[dependencies] [dependencies]
clap = { version = "4.5.40", features = ["derive", "env"] } clap = { version = "4.5.40", features = ["derive", "env"] }
console-subscriber = "0.4.1" console-subscriber = { version = "0.4.1", optional = true }
lb = { path = "crates/lb" } lb = { path = "crates/lb", features = ["runtime"] }
lb_sqlite = { path = "crates/lb_sqlite", optional = true }
luajit = { path = "crates/luajit", features = ["runtime"] } luajit = { path = "crates/luajit", features = ["runtime"] }
mimalloc = "0.1.47" mimalloc = "0.1.47"
owo-colors = "4.2.1" owo-colors = "4.2.1"
sysexits = "0.9.0" 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 = "0.1.41"
tracing-subscriber = "0.3.19" tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
[build-dependencies] [build-dependencies]
vergen-git2 = { version = "1.0.7", features = ["cargo", "rustc"] } vergen-git2 = { version = "1.0.7", features = ["cargo", "rustc"] }

View File

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

View File

@ -7,15 +7,21 @@ authors.workspace = true
homepage.workspace = true homepage.workspace = true
repository.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] [dependencies]
camino = "1.1.10"
derive_more = { version = "2.0.1", features = ["full"] } derive_more = { version = "2.0.1", features = ["full"] }
globset = { version = "0.4.16", optional = true }
luaffi = { path = "../luaffi" } luaffi = { path = "../luaffi" }
luaify = { path = "../luaify" }
luajit = { path = "../luajit" } luajit = { path = "../luajit" }
sysexits = "0.9.0" sysexits = "0.9.0"
tokio = { version = "1.45.1", features = ["rt", "time", "fs", "net", "process", "signal", "tracing"] } tempfile = { version = "3.20.0", optional = true }
tracing = "0.1.41" thiserror = "2.0.12"
tokio = { version = "1.45.1" }
[dev-dependencies] walkdir = { version = "2.5.0", optional = true }
luaify = { path = "../luaify" }
tokio = { version = "1.45.1", features = ["full"] }

View File

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

View File

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

83
crates/lb/src/fs/async.rs Normal file
View File

@ -0,0 +1,83 @@
use super::*;
use luaffi::{cdef, metatype};
use std::cell::RefCell;
use tokio::fs::{DirEntry, ReadDir};
/// Iterator over the entries in a directory.
#[derive(Debug)]
#[cdef]
pub struct lb_read_dir(#[opaque] RefCell<ReadDir>);
#[metatype]
impl lb_read_dir {
pub(super) fn new(iter: 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))
}
}
/// Entry inside of a directory on the filesystem.
#[derive(Debug)]
#[cdef]
pub struct lb_dir_entry(#[opaque] DirEntry);
#[metatype]
impl lb_dir_entry {
pub(super) fn new(entry: 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()
}
}

46
crates/lb/src/fs/glob.rs Normal file
View File

@ -0,0 +1,46 @@
use super::*;
use globset::GlobSet;
use luaffi::{cdef, metatype};
use std::cell::RefCell;
use walkdir::IntoIter;
/// Iterator that yields paths from the filesystem that match a particular pattern.
#[derive(Debug)]
#[cdef]
pub struct lb_glob_dir {
#[opaque]
iter: RefCell<IntoIter>,
#[opaque]
matcher: GlobSet,
#[opaque]
prefix: PathBuf,
}
#[metatype]
impl lb_glob_dir {
pub(super) fn new(iter: IntoIter, matcher: 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)
}
}

190
crates/lb/src/fs/mod.rs Normal file
View File

@ -0,0 +1,190 @@
//! Filesystem library.
//!
//! 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::{
cell::{BorrowError, BorrowMutError},
path::PathBuf,
};
use thiserror::Error;
mod r#async;
mod glob;
mod sync;
mod temp;
mod walk;
pub use r#async::*;
pub use glob::*;
pub use sync::*;
pub use temp::*;
pub use walk::*;
/// Errors that can be thrown by this library.
///
/// 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(module = "lb:fs")]
pub struct lb_fslib;
#[metatype]
impl lb_fslib {
#[new]
extern "Lua-C" fn new() -> Self {
Self
}
/// 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?)
}
/// 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 the current directory, yielding all entries within it.
pub extern "Lua" fn walk(pattern: &str) -> Result<lb_walk_dir> {
Self::walk_dir(".")
}
/// 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())
}
/// Recursively walks the current directory, yielding all entries within it that match the given
/// glob pattern.
///
/// # 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)
}
/// Recursively walks a directory, yielding all entries within it that match the given glob
/// pattern.
///
/// # 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)?))
}
}

251
crates/lb/src/fs/sync.rs Normal file
View File

@ -0,0 +1,251 @@
use super::*;
use luaffi::{cdef, metatype};
use std::{
cell::RefCell,
fs::{DirEntry, FileType, Metadata, Permissions, ReadDir},
time::SystemTime,
};
/// Structure representing the type of a file with accessors for each file type.
#[derive(Debug)]
#[cdef]
pub struct lb_file_type(#[opaque] FileType);
#[metatype]
impl lb_file_type {
pub(super) fn new(ty: 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] Metadata);
#[metatype]
impl lb_file_meta {
pub(super) fn new(meta: 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<Permissions>);
#[metatype]
impl lb_file_perms {
pub(super) fn new(perms: 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);
}
}
/// Synchronous version of [`lb_read_dir`].
#[derive(Debug)]
#[cdef]
pub struct lb_read_dir_sync(#[opaque] RefCell<ReadDir>);
#[metatype]
impl lb_read_dir_sync {
pub(super) fn new(iter: 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))
}
}
/// Synchronous version of [`lb_dir_entry`].
#[derive(Debug)]
#[cdef]
pub struct lb_dir_entry_sync(#[opaque] DirEntry);
#[metatype]
impl lb_dir_entry_sync {
pub(super) fn new(entry: 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()
}
}

25
crates/lb/src/fs/temp.rs Normal file
View File

@ -0,0 +1,25 @@
use luaffi::{cdef, metatype};
use tempfile::TempDir;
/// Directory in the filesystem that is automatically deleted when it is garbage-collected.
#[derive(Debug)]
#[cdef]
pub struct lb_temp_dir(#[opaque] TempDir);
#[metatype]
impl lb_temp_dir {
pub(super) fn new(dir: 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()
}
}

90
crates/lb/src/fs/walk.rs Normal file
View File

@ -0,0 +1,90 @@
use super::*;
use luaffi::{cdef, metatype};
use std::cell::RefCell;
use walkdir::{DirEntry, IntoIter};
/// Iterator for recursively descending into a directory.
#[derive(Debug)]
#[cdef]
pub struct lb_walk_dir(#[opaque] RefCell<IntoIter>);
#[metatype]
impl lb_walk_dir {
pub(super) fn new(iter: 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] DirEntry);
#[metatype]
impl lb_walk_dir_entry {
pub(super) fn new(entry: 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()
}
}

View File

@ -1,5 +1,11 @@
pub mod channel; //! luby core libraries.
#[cfg(feature = "fs")]
pub mod fs; pub mod fs;
#[cfg(feature = "net")]
pub mod net; pub mod net;
#[cfg(feature = "runtime")]
pub mod runtime; pub mod runtime;
#[cfg(feature = "task")]
pub mod task; pub mod task;
#[cfg(feature = "time")]
pub mod time;

View File

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

588
crates/lb/src/net/mod.rs Normal file
View File

@ -0,0 +1,588 @@
//! Network library.
//!
//! The `lb:net` library provides an asynchronous network API for woring with TCP, UDP and IPC
//! sockets.
//!
//! ## Exports
//!
//! See [`lb_netlib`] for items exported by this library.
use derive_more::{From, FromStr};
use luaffi::{cdef, marker::OneOf, metatype};
use std::{
cell::{BorrowError, BorrowMutError},
net::{AddrParseError, IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
};
use thiserror::Error;
use tokio::net::TcpSocket;
mod tcp;
pub use tcp::*;
/// Errors that can be thrown by this library.
///
/// 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),
/// IP or socket address syntax error.
#[error("{0}")]
InvalidAddr(#[from] AddrParseError),
/// Socket was already converted and can no longer be used.
#[error("socket was already converted")]
SocketConsumed,
/// Socket was closed and can no longer be used.
#[error("socket was closed")]
SocketClosed,
/// Specified socket half was not `"read"` or `"write"`.
#[error("invalid socket half")]
InvalidSocketHalf,
}
type Result<T> = std::result::Result<T, Error>;
/// Items exported by the `lb:net` library.
///
/// This library can be acquired by calling
/// [`require("lb:net")`](https://www.lua.org/manual/5.1/manual.html#pdf-require).
///
/// ```lua
/// local net = require("lb:net")
/// ```
#[cdef(module = "lb:net")]
pub struct lb_netlib;
#[metatype]
impl lb_netlib {
#[new]
extern "Lua-C" fn new() -> Self {
Self
}
/// An IPv4 address representing localhost: `127.0.0.1`
pub extern "Lua-C" fn localhost() -> lb_ipaddr {
lb_ipaddr(Ipv4Addr::LOCALHOST.into())
}
/// An IPv6 address representing localhost: `::1`
pub extern "Lua-C" fn localhost_v6() -> lb_ipaddr {
lb_ipaddr(Ipv6Addr::LOCALHOST.into())
}
/// An IPv4 address representing an unspecified address: `0.0.0.0`
pub extern "Lua-C" fn unspecified() -> lb_ipaddr {
lb_ipaddr(Ipv4Addr::UNSPECIFIED.into())
}
/// An IPv6 address representing an unspecified address: `::`
pub extern "Lua-C" fn unspecified_v6() -> lb_ipaddr {
lb_ipaddr(Ipv6Addr::UNSPECIFIED.into())
}
/// An IPv4 address representing the broadcast address: `255.255.255.255`
pub extern "Lua-C" fn broadcast() -> lb_ipaddr {
lb_ipaddr(Ipv4Addr::BROADCAST.into())
}
/// Creates an IP address from the given input.
///
/// If `addr` is an [`lb_ipaddr`], a copy of that value is returned. If `addr` is an
/// [`lb_socketaddr`], the IP part of the socket address is returned. Otherwise, parses `addr`
/// as an IP address string. Both IPv4 and IPv6 addresses are accepted.
///
/// An address string may be something like `127.0.0.1`.
///
/// The type of the parsed address can be checked using [`is_v4`](lb_ipaddr::is_v4) or
/// [`is_v6`](lb_ipaddr::is_v6) functions on the returned [`lb_ipaddr`](lb_ipaddr) value.
///
/// # Errors
///
/// This function will throw an error if the input syntax is invalid.
///
/// # Example
///
/// ```lua
/// local net = require("lb:net")
/// local addr = net.ipaddr("192.168.1.1")
///
/// assert(addr:is_v4())
/// ```
pub extern "Lua" fn ipaddr(
addr: OneOf<(&str, &lb_ipaddr, &lb_socketaddr)>,
) -> Result<lb_ipaddr> {
if __istype(__ct.lb_ipaddr, addr) {
__new(__ct.lb_ipaddr, addr) // copy constructor
} else if __istype(__ct.lb_socketaddr, addr) {
s.ip()
} else {
Self::__parse_ipaddr(addr)
}
}
extern "Lua-C" fn __parse_ipaddr(addr: &str) -> Result<lb_ipaddr> {
Ok(addr.parse()?)
}
/// Creates a socket address from the given input.
///
/// A socket address is an IP address with a prescribed port number.
///
/// If `addr` is an [`lb_socketaddr`], a copy of that value is returned. If `addr` is an
/// [`lb_ipaddr`], a socket address with that IP part is returned. Otherwise, parses `addr` as a
/// socket address string. Both IPv4 and IPv6 addresses are accepted.
///
/// If `port` is specified, it is always used as the port part of the returned socket address.
/// Otherwise, `0` is used as the default.
///
/// An address string may be something like `127.0.0.1:3000`.
///
/// # Errors
///
/// This function will throw an error if the input syntax is invalid.
///
/// # Example
///
/// ```lua
/// local net = require("lb:net")
/// local addr = net.socketaddr("::1", 8080)
///
/// assert(addr:ip():is_v6() and addr:ip():is_loopback())
/// assert(addr:port() == 8080)
/// ```
pub extern "Lua" fn socketaddr(
addr: OneOf<(&str, &lb_ipaddr, &lb_socketaddr)>,
port: Option<u16>,
) -> Result<lb_socketaddr> {
if port != () {
Self::__new_skaddr(Self::ipaddr(addr), port)
} else {
if __istype(__ct.lb_socketaddr, addr) {
__new(__ct.lb_socketaddr, addr) // copy constructor
} else if __istype(__ct.lb_ipaddr, addr) {
Self::__new_skaddr(addr, 0) // default port 0
} else {
Self::__parse_skaddr(addr)
}
}
}
extern "Lua-C" fn __new_skaddr(ip: &lb_ipaddr, port: u16) -> lb_socketaddr {
SocketAddr::new(ip.0, port).into()
}
extern "Lua-C" fn __parse_skaddr(addr: &str) -> Result<lb_socketaddr> {
Ok(if let Ok(addr) = addr.parse() {
SocketAddr::new(addr, 0).into() // default port 0
} else {
addr.parse::<SocketAddr>()?.into()
})
}
/// Creates a new TCP socket configured for IPv4.
///
/// This calls `socket(2)` with `AF_INET` and `SOCK_STREAM`.
///
/// # Errors
///
/// This function may throw an error if the socket could not be created.
pub extern "Lua-C" fn tcp() -> Result<lb_tcpsocket> {
Ok(lb_tcpsocket::new(TcpSocket::new_v4()?))
}
/// Creates a new TCP socket configured for IPv6.
///
/// This calls `socket(2)` with `AF_INET6` and `SOCK_STREAM`.
///
/// # Errors
///
/// This function may throw an error if the socket could not be created.
pub extern "Lua-C" fn tcp_v6() -> Result<lb_tcpsocket> {
Ok(lb_tcpsocket::new(TcpSocket::new_v6()?))
}
/// Creates a new TCP socket bound to the given address and port.
///
/// This function accepts the same arguments as [`socketaddr`](Self::socketaddr). It creates a
/// new TCP socket configured to use either IPv4 or IPv6 depending on the type of the given
/// address, and binds it to that address.
///
/// # Errors
///
/// This function may throw an error if the socket could not be created or bound to the
/// specified address.
///
/// # Example
///
/// ```lua
/// local net = require("lb:net")
/// local socket = net.bind_tcp("127.0.0.1")
///
/// assert(socket:local_addr():ip() == net.ipaddr("127.0.0.1"))
/// socket:set_nodelay(true)
/// ```
pub extern "Lua" fn bind_tcp(
addr: OneOf<(&str, &lb_ipaddr, &lb_socketaddr)>,
port: Option<u16>,
) -> Result<lb_tcpsocket> {
let addr = Self::socketaddr(addr, port);
let socket;
if addr.ip().is_v6() {
socket = Self::tcp_v6();
} else {
socket = Self::tcp();
}
socket.bind(addr);
socket
}
/// Creates a new TCP socket listening on the given address and port.
///
/// This is a convenience function that combines [`bind_tcp`](Self::bind_tcp) and
/// [`listen`](lb_tcpsocket::listen). It accepts the same arguments as
/// [`socketaddr`](Self::socketaddr).
///
/// # Errors
///
/// This function may throw an error if the socket could not be created, bound to the specified
/// address, or could not transition to the listening state.
///
/// # Example
///
/// ```lua,no_run
/// local net = require("lb:net")
/// local listener = net.listen_tcp("127.0.0.1")
///
/// assert(listener:local_addr():ip() == net.ipaddr("127.0.0.1"))
///
/// for stream in listener do
/// print("client connected: ", stream:peer_addr())
/// end
/// ```
pub extern "Lua" fn listen_tcp(
addr: OneOf<(&str, &lb_ipaddr, &lb_socketaddr)>,
port: Option<u16>,
) -> Result<lb_tcplistener> {
Self::bind_tcp(addr, port).listen(1024)
}
/// Establishes a new TCP connection to the server at the given address and port.
///
/// This function accepts the same arguments as [`socketaddr`](Self::socketaddr). It creates a
/// new TCP socket and connects it to the specified address.
///
/// # Errors
///
/// This function may throw an error if the socket could not be created or connected to the
/// specified address.
///
/// # Example
///
/// ```lua
/// local net = require("lb:net")
/// local listener = net.listen_tcp("127.0.0.1")
/// local stream = net.connect_tcp("127.0.0.1", listener:local_addr():port())
///
/// assert(stream:peer_addr():ip() == net.ipaddr("127.0.0.1"))
/// stream:write("Hello, server!\n")
/// ```
pub async extern "Lua" fn connect_tcp(
addr: OneOf<(&str, &lb_ipaddr, &lb_socketaddr)>,
port: Option<u16>,
) -> Result<lb_tcpstream> {
let addr = Self::socketaddr(addr, port);
let socket;
if addr.ip().is_v6() {
socket = Self::tcp_v6();
} else {
socket = Self::tcp();
}
socket.connect(addr)
}
}
/// IP address, either IPv4 or IPv6.
///
/// # Example
///
/// This example creates an [`lb_ipaddr`] by parsing an IP address string.
///
/// ```lua
/// local net = require("lb:net")
/// local addr = net.ipaddr("127.0.0.1") -- ipv4 loopback address
///
/// assert(addr:is_v4() and addr:is_loopback())
/// assert(tostring(addr) == "127.0.0.1")
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, From, FromStr)]
#[cdef]
pub struct lb_ipaddr(#[opaque] IpAddr);
#[metatype]
impl lb_ipaddr {
/// Returns `true` if this is an IPv4 address.
pub extern "Lua-C" fn is_v4(&self) -> bool {
self.0.is_ipv4()
}
/// Returns `true` if this is an IPv6 address.
pub extern "Lua-C" fn is_v6(&self) -> bool {
self.0.is_ipv6()
}
/// Returns the string `"v4"` if this is an IPv4 address, or `"v6"` if this is an IPv6 address.
pub extern "Lua" fn family(&self) -> String {
if self.is_v6() { "v6" } else { "v4" }
}
/// Returns `true` if this is a loopback address.
///
/// For IPv4, this is any address in the `127.0.0.0/8` range.
///
/// For IPv6, this is the address `::1`.
pub extern "Lua-C" fn is_loopback(&self) -> bool {
self.0.is_loopback()
}
/// Returns `true` if this address is unspecified.
///
/// For IPv4, this is the address `0.0.0.0`.
///
/// For IPv6, this is the address `::`.
pub extern "Lua-C" fn is_unspecified(&self) -> bool {
self.0.is_unspecified()
}
/// Returns `true` if this address is a multicast address.
///
/// For IPv4, this is any address in the `224.0.0.0/4` range.
///
/// For IPv6, this is any address in the `ff00::/8` range.
pub extern "Lua-C" fn is_multicast(&self) -> bool {
self.0.is_multicast()
}
/// Returns `true` if this is an IPv4 private address.
///
/// For IPv4, this is any address in one of these ranges:
///
/// - `10.0.0.0/8`
/// - `172.16.0.0/12`
/// - `192.168.0.0/16`
///
/// For IPv6, this always returns `false`.
pub extern "Lua-C" fn is_v4_private(&self) -> bool {
match self.0 {
IpAddr::V4(v4) => v4.is_private(),
IpAddr::V6(_) => false,
}
}
/// Returns `true` if this is an IPv4 link-local address.
///
/// For IPv4, this is any address in the `169.254.0.0/16` range.
///
/// For IPv6, this always returns `false`.
pub extern "Lua-C" fn is_v4_link_local(&self) -> bool {
match self.0 {
IpAddr::V4(v4) => v4.is_link_local(),
IpAddr::V6(_) => false,
}
}
/// Returns `true` if this is an IPv4 broadcast address.
///
/// For IPv4, this is the address `255.255.255.255`.
///
/// For IPv6, this always returns `false`.
pub extern "Lua-C" fn is_v4_broadcast(&self) -> bool {
match self.0 {
IpAddr::V4(v4) => v4.is_broadcast(),
IpAddr::V6(_) => false,
}
}
/// Returns `true` if this is an IPv4 documentation address.
///
/// For IPv4, this is any address in one of these ranges:
///
/// - `192.0.2.0/24` (TEST-NET-1)
/// - `198.51.100.0/24` (TEST-NET-2)
/// - `203.0.113.0/24` (TEST-NET-3)
///
/// For IPv6, this always returns `false`.
pub extern "Lua-C" fn is_v4_documentation(&self) -> bool {
match self.0 {
IpAddr::V4(v4) => v4.is_documentation(),
IpAddr::V6(_) => false,
}
}
/// Returns `true` if this is an IPv6 unique local address.
///
/// For IPv4, this always returns `false`.
///
/// For IPv6, this is any address in the `fc00::/7` range.
pub extern "Lua-C" fn is_v6_unique_local(&self) -> bool {
match self.0 {
IpAddr::V4(_) => false,
IpAddr::V6(v6) => v6.is_unique_local(),
}
}
/// Returns `true` if this is an IPv6 unicast address with link-local scope.
///
/// For IPv4, this always returns `false`.
///
/// For IPv6, this is any address in the `fe80::/10` range.
pub extern "Lua-C" fn is_v6_unicast_link_local(&self) -> bool {
match self.0 {
IpAddr::V4(_) => false,
IpAddr::V6(v6) => v6.is_unicast_link_local(),
}
}
/// Converts this address to IPv4.
///
/// For IPv4, this returns the address unchanged.
///
/// For IPv6, this returns the original IPv4 address if it is an IPv4-mapped or IPv4-compatible
/// IPv6 address. Otherwise, this returns `nil`.
pub extern "Lua-C" fn to_v4(&self) -> Option<Self> {
match self.0 {
IpAddr::V4(_) => Some(*self),
IpAddr::V6(v6) => v6.to_ipv4().map(|v| Self(v.into())),
}
}
/// Converts this address to IPv6.
///
/// For IPv4, this returns the IPv4-mapped IPv6 address as defined by
/// [`Ipv4Addr::to_ipv6_mapped`].
///
/// For IPv6, this returns the address unchanged.
pub extern "Lua-C" fn to_v6(&self) -> Self {
match self.0 {
IpAddr::V4(v4) => Self(v4.to_ipv6_mapped().into()),
IpAddr::V6(_) => *self,
}
}
/// Returns the canonical form of this address.
///
/// For IPv4, this returns the address unchanged.
///
/// For IPv6, this returns the original IPv4 address if it is an IPv4-mapped or IPv4-compatible
/// IPv6 address. Otherwise, this returns the address unchanged.
pub extern "Lua-C" fn to_canonical(&self) -> Self {
match self.0 {
IpAddr::V4(_) => *self,
IpAddr::V6(v6) => v6.to_ipv4().map_or(*self, |v| Self(v.into())),
}
}
/// Returns `true` if the given addresses are equal.
///
/// Two addresses are considered equal if they are of the same family (IPv4 or IPv6) and
/// represent the same address in octets.
#[eq]
pub extern "Lua-C" fn equals(left: &Self, right: &Self) -> bool {
left.0 == right.0
}
/// Returns `true` if the left address is less than the right address.
///
/// IPv4 addresses are always less than IPv6 addresses.
#[lt]
pub extern "Lua-C" fn less_than(left: &Self, right: &Self) -> bool {
left.0 < right.0
}
/// Returns `true` if the left address is less than or equal to the right address.
///
/// IPv4 addresses are always less than IPv6 addresses.
#[le]
pub extern "Lua-C" fn less_than_or_equals(left: &Self, right: &Self) -> bool {
left.0 <= right.0
}
/// Returns the string representation of this address.
#[tostring]
pub extern "Lua-C" fn tostring(&self) -> String {
self.0.to_string()
}
}
/// Socket address, which is an IP address with a port number.
///
/// This represents an IP address with a prescribed port, such as `127.0.0.1:8080` or `[::1]:443`.
/// It is used to specify endpoints for network connections and listeners, and can be constructed by
/// [`socketaddr`](lb_netlib::socketaddr).
///
/// # Example
///
/// ```lua
/// local net = require("lb:net")
/// local addr = net.socketaddr("127.0.0.1:8080")
///
/// assert(addr:ip() == net.ipaddr("127.0.0.1") and addr:port() == 8080)
/// assert(tostring(addr) == "127.0.0.1:8080")
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, From, FromStr)]
#[cdef]
pub struct lb_socketaddr(#[opaque] SocketAddr);
#[metatype]
impl lb_socketaddr {
/// The IP part of this address.
pub extern "Lua-C" fn ip(&self) -> lb_ipaddr {
self.0.ip().into()
}
/// The port part of this address.
pub extern "Lua-C" fn port(&self) -> u16 {
self.0.port()
}
/// Returns a new socket address with the given IP address and the same port.
pub extern "Lua-C" fn with_ip(&self, ip: &lb_ipaddr) -> Self {
SocketAddr::new(ip.0, self.port()).into()
}
/// Returns a new socket address with the given port and the same IP address.
pub extern "Lua-C" fn with_port(&self, port: u16) -> Self {
SocketAddr::new(self.ip().0, port).into()
}
/// Returns `true` if the given addresses are equal.
#[eq]
pub extern "Lua-C" fn equals(left: &Self, right: &Self) -> bool {
left.0 == right.0
}
/// Returns `true` if the left address is less than the right address.
#[lt]
pub extern "Lua-C" fn less_than(left: &Self, right: &Self) -> bool {
left.0 < right.0
}
/// Returns `true` if the left address is less than or equal to the right address.
#[le]
pub extern "Lua-C" fn less_than_or_equals(left: &Self, right: &Self) -> bool {
left.0 <= right.0
}
/// Returns the string representation of this address.
#[tostring]
pub extern "Lua-C" fn tostring(&self) -> String {
self.0.to_string()
}
}

622
crates/lb/src/net/tcp.rs Normal file
View File

@ -0,0 +1,622 @@
use super::*;
use luaffi::{cdef, marker::fun, metatype};
use luajit::LUA_NOREF;
use std::io::ErrorKind;
use std::{
cell::{Ref, RefCell, RefMut},
ffi::c_int,
time::Duration,
};
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt, Interest},
net::{TcpListener, TcpSocket, TcpStream},
};
/// TCP socket which has not yet been converted to an [`lb_tcpstream`] or [`lb_tcplistener`].
///
/// This type represents a TCP socket in its initial state, before it is connected or set to listen.
/// It can be configured (e.g., socket options, bind address) before being converted to an
/// [`lb_tcpstream`] (via [`connect`](lb_tcpsocket::connect)) or [`lb_tcplistener`] (via
/// [`listen`](lb_tcpsocket::listen)), after which it can no longer be used.
///
/// Methods on this type may fail if the operating system does not support the requested operation.
#[derive(Debug)]
#[cdef]
pub struct lb_tcpsocket(#[opaque] RefCell<Option<TcpSocket>>);
#[metatype]
impl lb_tcpsocket {
pub(super) fn new(socket: TcpSocket) -> Self {
Self(RefCell::new(Some(socket)))
}
fn socket<'s>(&'s self) -> Result<Ref<'s, TcpSocket>> {
let socket = self.0.borrow();
match *socket {
Some(_) => Ok(Ref::map(socket, |s| s.as_ref().unwrap())),
None => Err(Error::SocketConsumed),
}
}
/// Gets the value of the `SO_KEEPALIVE` option on this socket.
pub extern "Lua-C" fn keepalive(&self) -> Result<bool> {
Ok(self.socket()?.keepalive()?)
}
/// Sets value for the `SO_KEEPALIVE` option on this socket.
///
/// This enables or disables periodic keepalive messages on the connection.
pub extern "Lua-C" fn set_keepalive(&self, enabled: bool) -> Result<()> {
Ok(self.socket()?.set_keepalive(enabled)?)
}
/// Gets the value of the `SO_REUSEADDR` option on this socket.
pub extern "Lua-C" fn reuseaddr(&self) -> Result<bool> {
Ok(self.socket()?.reuseaddr()?)
}
/// Sets value for the `SO_REUSEADDR` option on this socket.
///
/// This allows the socket to bind to an address that is already in use.
pub extern "Lua-C" fn set_reuseaddr(&self, enabled: bool) -> Result<()> {
Ok(self.socket()?.set_reuseaddr(enabled)?)
}
/// Gets the value of the `SO_REUSEPORT` option on this socket.
pub extern "Lua-C" fn reuseport(&self) -> Result<bool> {
Ok(self.socket()?.reuseport()?)
}
/// Sets value for the `SO_REUSEPORT` option on this socket.
///
/// This allows multiple sockets to bind to the same port.
pub extern "Lua-C" fn set_reuseport(&self, enabled: bool) -> Result<()> {
Ok(self.socket()?.set_reuseport(enabled)?)
}
/// Gets the value of the `SO_SNDBUF` option on this socket.
pub extern "Lua-C" fn sendbuf(&self) -> Result<u32> {
Ok(self.socket()?.send_buffer_size()?)
}
/// Sets value for the `SO_SNDBUF` option on this socket.
///
/// This sets the size of the send buffer in bytes.
pub extern "Lua-C" fn set_sendbuf(&self, size: u32) -> Result<()> {
Ok(self.socket()?.set_send_buffer_size(size)?)
}
/// Gets the value of the `SO_RCVBUF` option on this socket.
pub extern "Lua-C" fn recvbuf(&self) -> Result<u32> {
Ok(self.socket()?.recv_buffer_size()?)
}
/// Sets value for the `SO_RCVBUF` option on this socket.
///
/// This sets the size of the receive buffer in bytes.
pub extern "Lua-C" fn set_recvbuf(&self, size: u32) -> Result<()> {
Ok(self.socket()?.set_recv_buffer_size(size)?)
}
/// Gets the value of the `SO_LINGER` option on this socket, in seconds.
pub extern "Lua-C" fn linger(&self) -> Result<f64> {
Ok(self
.socket()?
.linger()?
.map(|n| n.as_secs_f64())
.unwrap_or(0.))
}
/// Sets the value of the `SO_LINGER` option on this socket.
///
/// This controls how long the socket will remain open after close if unsent data is present.
pub extern "Lua-C" fn set_linger(&self, secs: f64) -> Result<()> {
let secs = secs.max(0.);
Ok(self
.socket()?
.set_linger((secs != 0.).then_some(Duration::from_secs_f64(secs)))?)
}
/// Gets the value of the `TCP_NODELAY` option on this socket.
pub extern "Lua-C" fn nodelay(&self) -> Result<bool> {
Ok(self.socket()?.nodelay()?)
}
/// Sets the value of the `TCP_NODELAY` option on this socket.
///
/// This enables or disables Nagle's algorithm, which delays sending small packets.
pub extern "Lua-C" fn set_nodelay(&self, enabled: bool) -> Result<()> {
Ok(self.socket()?.set_nodelay(enabled)?)
}
/// Gets the local address that this socket is bound to.
pub extern "Lua-C" fn local_addr(&self) -> Result<lb_socketaddr> {
Ok(self.socket()?.local_addr()?.into())
}
/// Binds this socket to the given local address.
pub extern "Lua-C" fn bind(&self, addr: &lb_socketaddr) -> Result<()> {
Ok(self.socket()?.bind(addr.0)?)
}
/// Transitions this socket to the listening state.
///
/// This consumes the socket and returns a new [`lb_tcplistener`] that can accept incoming
/// connections. This socket object can no longer be used after this call.
pub extern "Lua-C" fn listen(&self, backlog: u32) -> Result<lb_tcplistener> {
let socket = self.0.borrow_mut().take().ok_or(Error::SocketConsumed)?;
Ok(lb_tcplistener::new(socket.listen(backlog)?))
}
/// Connects this socket to the given remote socket address, transitioning it to an established
/// state.
///
/// This consumes the socket and returns a new [`lb_tcpstream`] that can be used to send and
/// receive data. This socket object can no longer be used after this call.
///
/// # Errors
///
/// This function may throw an error if connection could not be established to the given remote
/// address.
pub async extern "Lua-C" fn connect(&self, addr: &lb_socketaddr) -> Result<lb_tcpstream> {
let socket = self.0.borrow_mut().take().ok_or(Error::SocketConsumed)?;
Ok(lb_tcpstream::new(socket.connect(addr.0).await?))
}
}
/// TCP connection between a local and a remote socket.
///
/// This represents an established TCP connection. It is created by connecting an [`lb_tcpsocket`]
/// to a remote socket (via [`connect`](lb_tcpsocket::connect)) or accepting a connection from an
/// [`lb_tcplistener`] (via [`accept`](lb_tcplistener::accept)). It provides methods for reading
/// from and writing to the stream asynchronously.
///
/// The stream supports reading and writing data in both directions concurrently. Typically you
/// would spawn one reader task and one writer task to handle incoming and outgoing data
/// respectively. Connection is closed when this object goes out of scope and gets garbage
/// collected, or when [`close`](Self::close) is explicitly called.
///
/// Methods on this type may fail if the operating system does not support the requested operation.
///
/// # Example
///
/// This examples spawns a reader task and a writer task to operate on the stream concurrently.
///
/// ```lua,no_run
/// local task = require("lb:task")
/// local net = require("lb:net")
/// local socket = net.connect_tcp("127.0.0.1:1234")
///
/// print("local address: ", socket:local_addr())
/// print("remote address: ", socket:peer_addr())
///
/// local reader = spawn(function()
/// for chunk in socket, 1024 do
/// print("received: ", chunk)
/// end
///
/// print("done reading")
/// end)
///
/// local writer = spawn(function()
/// for i = 1, 10 do
/// local msg = ("message %d"):format(i)
/// socket:write(msg)
/// print("sent: ", msg)
/// end
///
/// print("done writing")
/// end)
///
/// task.join(reader, writer)
/// ```
///
/// The above example uses the socket as an iterator in a generic `for` loop to read data in chunks
/// of up to 1024 bytes. It is equivalent to the following:
///
/// ```lua,no_run
/// local net = require("lb:net")
/// local socket = net.connect_tcp("127.0.0.1:1234")
///
/// while true do
/// local chunk = socket:read_partial(1024)
/// if chunk == nil then break end
/// print("received: ", chunk)
/// end
/// ```
#[derive(Debug)]
#[cdef]
pub struct lb_tcpstream {
#[opaque]
read: RefCell<Option<OwnedReadHalf>>,
#[opaque]
write: RefCell<Option<OwnedWriteHalf>>,
}
#[metatype]
impl lb_tcpstream {
pub(super) fn new(stream: TcpStream) -> Self {
let (read, write) = stream.into_split();
Self {
read: RefCell::new(Some(read)),
write: RefCell::new(Some(write)),
}
}
fn read_half<'s>(&'s self) -> Result<RefMut<'s, OwnedReadHalf>> {
let read = self.read.try_borrow_mut()?;
match *read {
Some(_) => Ok(RefMut::map(read, |s| s.as_mut().unwrap())),
None => Err(Error::SocketClosed),
}
}
fn write_half<'s>(&'s self) -> Result<RefMut<'s, OwnedWriteHalf>> {
let write = self.write.try_borrow_mut()?;
match *write {
Some(_) => Ok(RefMut::map(write, |s| s.as_mut().unwrap())),
None => Err(Error::SocketClosed),
}
}
/// The local socket address that this stream is bound to.
pub extern "Lua-C" fn local_addr(&self) -> Result<lb_socketaddr> {
Ok(self.read_half()?.local_addr()?.into())
}
/// The remote socket address that this stream is connected to.
pub extern "Lua-C" fn peer_addr(&self) -> Result<lb_socketaddr> {
Ok(self.read_half()?.peer_addr()?.into())
}
/// Waits for this stream to be ready in the given half.
///
/// The argument `half` can be `"read"` for the readable half, `"write"` for the writable half,
/// or `nil` for both.
pub async extern "Lua-C" fn ready(&self, half: Option<&str>) -> Result<()> {
self.read_half()?
.ready(match half {
Some("read") => Interest::READABLE,
Some("write") => Interest::WRITABLE,
None => Interest::READABLE | Interest::WRITABLE,
_ => Err(Error::InvalidSocketHalf)?,
})
.await?;
Ok(())
}
/// Closes this stream in the given half.
///
/// The argument `half` can be `"read"` for the readable half, `"write"` for the writable half,
/// or `nil` for both.
///
/// Once the half is closed, it can no longer be used for reading or writing for that half. Once
/// both halves are closed, the stream is fully shut down.
pub extern "Lua-C" fn close(&self, half: Option<&str>) -> Result<()> {
Ok(match half {
Some("read") => drop(self.read.try_borrow_mut()?.take()),
Some("write") => drop(self.write.try_borrow_mut()?.take()),
None => drop((
self.read.try_borrow_mut()?.take(),
self.write.try_borrow_mut()?.take(),
)),
_ => Err(Error::InvalidSocketHalf)?,
})
}
fn is_disc(err: ErrorKind) -> bool {
matches!(
err,
ErrorKind::ConnectionReset // graceful shutdown
| ErrorKind::BrokenPipe // abrupt shutdown
| ErrorKind::UnexpectedEof // could not read requested amount of data
| ErrorKind::WriteZero // could not write requested amount of data
)
}
/// Reads exactly `len` bytes from this stream.
///
/// If the connection was closed, this returns `nil`.
pub async extern "Lua-C" fn read(&self, len: u32) -> Result<Option<Vec<u8>>> {
let mut buf = vec![0; len as usize];
Ok(match self.read_half()?.read_exact(&mut buf).await {
Ok(_) => Some(buf),
Err(err) if Self::is_disc(err.kind()) => None,
Err(err) => return Err(err.into()),
})
}
/// Reads up to `len` bytes from this stream.
///
/// The returned bytes may be less than `len` in length if the stream had less data available in
/// queue. If there was no data available or the connection was closed, this returns `nil`.
pub async extern "Lua-C" fn read_partial(&self, len: u32) -> Result<Option<Vec<u8>>> {
let mut buf = vec![0; len as usize];
Ok(match self.read_half()?.read(&mut buf).await {
Ok(0) => None,
Ok(n) => Some({
buf.truncate(n);
buf
}),
Err(err) if Self::is_disc(err.kind()) => None,
Err(err) => return Err(err.into()),
})
}
/// Attempts to read up to `len` bytes from this stream without waiting.
///
/// The returned bytes may be less than `len` in length if the stream had less data available in
/// queue. If there was no data available or the connection was closed, this returns `nil`.
pub extern "Lua-C" fn try_read(&self, len: u32) -> Result<Option<Vec<u8>>> {
let mut buf = vec![0; len as usize];
Ok(match self.read_half()?.try_read(&mut buf) {
Ok(0) => None,
Ok(n) => Some({
buf.truncate(n);
buf
}),
Err(err) if Self::is_disc(err.kind()) || err.kind() == ErrorKind::WouldBlock => None,
Err(err) => return Err(err.into()),
})
}
/// Writes exactly the given bytes to this stream.
///
/// If the connection was closed, this returns `false`.
pub async extern "Lua-C" fn write(&self, buf: &[u8]) -> Result<bool> {
Ok(match self.write_half()?.write_all(buf).await {
Ok(()) => true,
Err(err) if Self::is_disc(err.kind()) => false,
Err(err) => return Err(err.into()),
})
}
/// Writes the given bytes to this stream, and returns the number of bytes successfully written.
///
/// The returned number may be less than the length of `buf` if there was not enough space in
/// queue. If the connection was closed, this returns `nil`.
pub async extern "Lua-C" fn write_partial(&self, buf: &[u8]) -> Result<Option<u32>> {
Ok(match self.write_half()?.write(buf).await {
Ok(n) => Some(n as u32),
Err(err) if Self::is_disc(err.kind()) => None,
Err(err) => return Err(err.into()),
})
}
/// Attempts to write the given bytes to this stream without waiting, and returns the number of
/// bytes successfully written.
///
/// The returned number may be less than the length of `buf` if there was not enough space in
/// queue. If the connection was closed, this returns `nil`.
pub extern "Lua-C" fn try_write(&self, buf: &[u8]) -> Result<Option<u32>> {
Ok(match self.write_half()?.try_write(buf) {
Ok(n) => Some(n as u32),
Err(err) if Self::is_disc(err.kind()) || err.kind() == ErrorKind::WouldBlock => None,
Err(err) => return Err(err.into()),
})
}
/// Peeks up to `len` bytes at incoming data without consuming it.
///
/// Successive calls will return the same data until it is consumed by the [`read*`](Self::read)
/// family of functions.
pub async extern "Lua-C" fn peek(&self, len: u32) -> Result<Option<Vec<u8>>> {
let mut buf = vec![0; len as usize];
Ok(match self.read_half()?.peek(&mut buf).await {
Ok(0) => None,
Ok(n) => Some({
buf.truncate(n);
buf
}),
Err(err) if Self::is_disc(err.kind()) => None,
Err(err) => return Err(err.into()),
})
}
/// Alias for [`read_partial`](Self::read_partial).
#[call]
pub async extern "Lua" fn call(&self, len: u32) -> Result<Option<Vec<u8>>> {
self.read_partial(len)
}
}
/// TCP socket server, listening for connections.
///
/// This type represents a TCP server socket that can accept incoming connections. It is created by
/// transitioning an [`lb_tcpsocket`] to the listening state via [`listen`](lb_tcpsocket::listen).
///
/// Methods on this type may fail if the operating system does not support the requested operation.
///
/// # Example
///
/// The listener can be used as an iterator in a generic `for` loop to accept incoming connections:
///
/// ```lua,no_run
/// local net = require("lb:net")
/// local listener = net.listen_tcp("127.0.0.1")
///
/// print("listening on: ", listener:local_addr())
///
/// for stream in listener do
/// print("accepted connection from: ", stream:peer_addr())
/// print("local address: ", stream:local_addr())
///
/// spawn(function()
/// stream:write("hello from server\n")
/// stream:close()
/// end)
/// end
/// ```
#[derive(Debug)]
#[cdef]
pub struct lb_tcplistener {
#[opaque]
listener: TcpListener,
__on_accept_ref: c_int,
}
#[metatype]
impl lb_tcplistener {
pub(super) fn new(listener: TcpListener) -> Self {
Self {
listener,
__on_accept_ref: LUA_NOREF,
}
}
/// The local socket address that this listener is bound to.
pub extern "Lua-C" fn local_addr(&self) -> Result<lb_socketaddr> {
Ok(self.listener.local_addr()?.into())
}
/// Registers a callback to be invoked with each new incoming connection before it is converted
/// to an [`lb_tcpstream`].
///
/// The callback receives a temporary [`lb_tcplistener_stream`] object, which can be used to log
/// incoming connections or configure socket options (such as
/// [`set_nodelay`](lb_tcplistener_stream), [`set_linger`](lb_tcplistener_stream), etc.) before
/// it is converted to an [`lb_tcpstream`]. The callback is called synchronously during
/// [`accept`](Self::accept) and should complete as quickly as possible. The provided
/// configurable object is only valid within the callback and is converted to an
/// [`lb_tcpstream`] as soon as it returns.
///
/// If a callback already exists, it is replaced with the new one.
///
/// # Example
///
/// ```lua
/// local net = require("lb:net")
/// local listener = net.listen_tcp("127.0.0.1")
///
/// listener:on_accept(function(stream)
/// print("accepted connection from: ", stream:peer_addr())
/// print("local address: ", stream:local_addr())
///
/// stream:set_nodelay(true)
/// end)
/// ```
pub extern "Lua" fn on_accept(&self, cb: fun<(&lb_tcplistener_stream,), ()>) {
assert(
rawequal(cb, ()) || r#type(cb) == "function",
concat!("function expected in argument 'cb', got ", r#type(cb)),
);
__unref(self.__on_accept_ref);
self.__on_accept_ref = __ref(cb);
}
/// Accepts a new incoming TCP connection.
///
/// If an [`on_accept`](Self::on_accept) callback is registered, it is invoked with a temporary
/// [`lb_tcplistener_stream`] object representing the new connection. This allows configuration
/// of socket options for this specific connection, before the stream is converted to an
/// [`lb_tcpstream`] and returned for the connection to be read from or written to.
pub async extern "Lua" fn accept(&self) -> Result<lb_tcpstream> {
let stream = self.__accept();
let on_accept = __registry[self.__on_accept_ref];
if !rawequal(on_accept, ()) {
on_accept(stream);
}
stream.__convert()
}
async extern "Lua-C" fn __accept(&self) -> Result<lb_tcplistener_stream> {
let (stream, _) = self.listener.accept().await?;
Ok(lb_tcplistener_stream::new(stream))
}
/// Alias for [`accept`](Self::accept).
#[call]
pub async extern "Lua" fn call(&self) -> Result<lb_tcpstream> {
self.accept()
}
#[gc]
extern "Lua" fn gc(&self) {
__unref(self.__on_accept_ref);
}
}
/// TCP connection that has just been accepted by [`lb_tcplistener`].
///
/// This type is passed to the [`on_accept`](lb_tcplistener::on_accept) callback on
/// [`lb_tcplistener`], allowing socket options to be set before the stream is converted to an
/// [`lb_tcpstream`]. After conversion, this object can no longer be used.
///
/// Methods on this type may fail if the operating system does not support the requested operation.
#[derive(Debug)]
#[cdef]
pub struct lb_tcplistener_stream(#[opaque] RefCell<Option<TcpStream>>);
#[metatype]
impl lb_tcplistener_stream {
fn new(stream: TcpStream) -> Self {
Self(RefCell::new(Some(stream)))
}
fn stream<'s>(&'s self) -> Result<Ref<'s, TcpStream>> {
let socket = self.0.borrow();
match *socket {
Some(_) => Ok(Ref::map(socket, |s| s.as_ref().unwrap())),
None => Err(Error::SocketConsumed),
}
}
/// The local socket address that the listener is bound to.
pub extern "Lua-C" fn local_addr(&self) -> Result<lb_socketaddr> {
Ok(self.stream()?.local_addr()?.into())
}
/// The remote socket address that this stream is connected to.
pub extern "Lua-C" fn peer_addr(&self) -> Result<lb_socketaddr> {
Ok(self.stream()?.peer_addr()?.into())
}
/// Gets the value of the `TCP_NODELAY` option on this stream.
pub extern "Lua-C" fn nodelay(&self) -> Result<bool> {
Ok(self.stream()?.nodelay()?)
}
/// Sets the value of the `TCP_NODELAY` option on this stream.
///
/// This enables or disables Nagle's algorithm, which delays sending small packets.
pub extern "Lua-C" fn set_nodelay(&self, enabled: bool) -> Result<()> {
Ok(self.stream()?.set_nodelay(enabled)?)
}
/// Gets the value of the `SO_LINGER` option on this stream, in seconds.
pub extern "Lua-C" fn linger(&self) -> Result<f64> {
Ok(self
.stream()?
.linger()?
.map(|n| n.as_secs_f64())
.unwrap_or(0.))
}
/// Sets the value of the `SO_LINGER` option on this stream.
///
/// This controls how long the stream will remain open after close if unsent data is present.
pub extern "Lua-C" fn set_linger(&self, secs: f64) -> Result<()> {
let secs = secs.max(0.);
Ok(self
.stream()?
.set_linger((secs != 0.).then_some(std::time::Duration::from_secs_f64(secs)))?)
}
/// Gets the value of the `IP_TTL` option for this stream.
pub extern "Lua-C" fn ttl(&self) -> Result<u32> {
Ok(self.stream()?.ttl()?)
}
/// Sets the value for the `IP_TTL` option on this stream.
pub extern "Lua-C" fn set_ttl(&self, ttl: u32) -> Result<()> {
Ok(self.stream()?.set_ttl(ttl)?)
}
extern "Lua-C" fn __convert(&self) -> Result<lb_tcpstream> {
Ok(lb_tcpstream::new(
self.0.borrow_mut().take().ok_or(Error::SocketConsumed)?,
))
}
}

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 derive_more::{Deref, DerefMut};
use luaffi::{Registry, Type}; use luaffi::{Module, Registry};
use luajit::{Chunk, State}; use luaify::luaify_chunk;
use std::fmt::Display; use luajit::{Chunk, Index, NewTable, State};
use std::rc::Rc;
use tokio::{ use tokio::{
task::{JoinHandle, LocalSet, futures::TaskLocalFuture, spawn_local}, task::{JoinHandle, LocalSet, futures::TaskLocalFuture, spawn_local},
task_local, task_local,
}; };
#[derive(Debug, Default)] pub type ErrorFn = dyn Fn(&luajit::Error);
pub struct Builder { pub struct Builder {
registry: Registry, registry: Registry,
report_err: Rc<ErrorFn>,
jit_opts: Vec<String>,
prohibit_globals: bool,
} }
impl Builder { impl Builder {
pub fn new() -> Self { pub fn new() -> Self {
let mut registry = Registry::new(); Self {
registry: Registry::new(),
registry report_err: Rc::new(|err| match err.trace() {
.preload::<lb_libtask>("lb:task") Some(trace) => eprintln!("unhandled lua error: {err}\n{trace}"),
.preload::<lb_libchannel>("lb:channel") None => eprintln!("unhandled lua error: {err}"),
.preload::<lb_libfs>("lb:fs") }),
.preload::<lb_libnet>("lb:net"); jit_opts: vec![
// more aggressive jit options based on OpenResty's defaults
Self { registry } // 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 module<T: Type>(&mut self, name: impl Display) -> &mut Self {
self.registry.preload::<T>(name);
self
} }
pub fn registry(&self) -> &Registry { pub fn registry(&self) -> &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> { 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 { Ok(Runtime {
state: { cx: Context {
let mut s = State::new()?; state,
let mut chunk = Chunk::new(self.registry.done()); report_err: self.report_err.clone(),
chunk.extend(include_bytes!("./runtime.lua"));
s.eval(chunk.path("[luby]"), 0, 0)?;
s
}, },
tasks: LocalSet::new(), 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 { pub struct Runtime {
#[deref] #[deref]
#[deref_mut] #[deref_mut]
state: State, cx: Context,
tasks: LocalSet, tasks: LocalSet,
} }
task_local! {
static STATE: State;
}
impl Runtime { impl Runtime {
pub fn spawn<T: 'static>( pub fn spawn<T: 'static>(
&self, &self,
f: impl AsyncFnOnce(&mut State) -> T + 'static, f: impl AsyncFnOnce(&mut Context) -> T + 'static,
) -> JoinHandle<T> { ) -> JoinHandle<T> {
self.tasks 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 { impl IntoFuture for Runtime {
type Output = (); type Output = ();
type IntoFuture = TaskLocalFuture<State, LocalSet>; type IntoFuture = TaskLocalFuture<Context, LocalSet>;
fn into_future(self) -> Self::IntoFuture { 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 provides utilities for the scheduling of and communication between
//! asynchronous tasks.
//!
//! ## Exports
//!
//! See [`lb_tasklib`] for items exported by this library.
use crate::runtime::spawn; use crate::runtime::spawn;
use luaffi::{cdef, metatype}; use luaffi::{
use std::{ffi::c_int, process}; cdef,
use tokio::task::JoinHandle; marker::{function, many},
metatype,
};
use std::{cell::RefCell, ffi::c_int, time::Duration};
use tokio::{task::JoinHandle, time::sleep};
#[cdef] /// Items exported by the `lb:task` library.
pub struct lb_libtask; ///
/// 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] #[metatype]
impl lb_libtask { #[include("task.lua")]
impl lb_tasklib {
#[new] #[new]
extern "Lua-C" fn new() -> Self { extern "Lua-C" fn new() -> Self {
Self Self
} }
pub extern "Lua" fn spawn(self, f: function, ...) { pub async extern "Lua-C" fn sleep(ms: f64) {
// pack the function and its arguments into a table and pass its ref to rust sleep(Duration::from_secs_f64(ms.max(0.) / 1000.)).await;
self.__spawn(__ref(__tpack(f, variadic!())))
} }
extern "Lua-C" fn __spawn(&self, key: c_int) -> lb_task { pub extern "Lua" fn spawn(f: function, ...) -> lb_task {
let handle = spawn(async move |s| { // pack the function and its arguments into a table and pass its ref to rust.
// SAFETY: key is always unique, created by __ref above //
let arg = unsafe { s.new_ref_unchecked(key) }; // this "state" table is used from rust-side to call the function with its args, and it's
s.resize(0); // also reused to store its return values that the task handle can return when awaited. the
s.push(arg); // ref is owned by the task handle and unref'ed when it's gc'ed.
let narg = s.unpack(1, 1, None) - 1; assert(
println!("{s:?}"); r#type(f) == "function",
if let Err(_err) = s.call_async(narg, 0).await { concat!("function expected in argument 'f', got ", r#type(f)),
process::exit(1) );
// 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(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] #[cdef]
pub struct lb_task { pub struct lb_task {
#[opaque] #[opaque]
handle: JoinHandle<()>, handle: RefCell<Option<JoinHandle<()>>>,
__ref: c_int,
} }
#[metatype] #[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);
}
}

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

@ -0,0 +1,49 @@
//! Time library.
//!
//! The `lb:time` library provides utilities for working with the date and time and its related
//! constructs.
//!
//! ## Exports
//!
//! See [`lb_timelib`] for items exported by this library.
use luaffi::{cdef, metatype};
/// Items exported by the `lb:time` library.
///
/// This library can be acquired by calling
/// [`require("lb:time")`](https://www.lua.org/manual/5.1/manual.html#pdf-require).
///
/// ```lua
/// local time = require("lb:time")
/// ```
#[cdef(module = "lb:time")]
pub struct lb_timelib;
#[metatype]
impl lb_timelib {
#[new]
extern "Lua-C" fn new() -> Self {
Self
}
/// Returns an instant object that represents the current time at the time of calling.
pub extern "Lua-C" fn instant() -> lb_instant {
lb_instant::new(std::time::Instant::now())
}
}
/// Represents the measurement of a monotonically nondecreasing clock.
#[cdef]
pub struct lb_instant(#[opaque] std::time::Instant);
#[metatype]
impl lb_instant {
fn new(instant: std::time::Instant) -> Self {
Self(instant)
}
/// Returns the number of seconds elapsed since this instant was measured.
pub extern "Lua-C" fn elapsed_secs(&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)

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

@ -0,0 +1,184 @@
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:close()
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:close()
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:close()
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:close()
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")
-- close
server_stream:close()
client_stream:close()
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

@ -0,0 +1,13 @@
[package]
name = "lb_sqlite"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
[dependencies]
lb = { path = "../lb", features = ["time"] }
luaffi = { version = "0.0.1", path = "../luaffi" }
rusqlite = { version = "0.36.0", features = ["bundled", "load_extension", "backup", "blob", "limits", "window", "series", "session", "collation", "serialize"] }

View File

@ -0,0 +1,3 @@
//! luby SQLite library.
#[path = "mod.rs"]
pub mod sqlite;

View File

@ -0,0 +1,27 @@
//! SQLite library.
//!
//! The `lb:sqlite` library provides an interface to SQLite databases.
//!
//! ## Exports
//!
//! See [`lb_sqlitelib`] for items exported by this library.
use luaffi::{cdef, metatype};
/// Items exported by the `lb:sqlite` library.
///
/// This library can be acquired by calling
/// [`require("lb:sqlite")`](https://www.lua.org/manual/5.1/manual.html#pdf-require).
///
/// ```lua
/// local sqlite = require("lb:sqlite")
/// ```
#[cdef(module = "lb:sqlite")]
pub struct lb_sqlitelib;
#[metatype]
impl lb_sqlitelib {
#[new]
extern "Lua-C" fn new() -> Self {
Self
}
}

View File

@ -7,6 +7,10 @@ authors.workspace = true
homepage.workspace = true homepage.workspace = true
repository.workspace = true repository.workspace = true
[features]
option_ref_abi = []
option_string_abi = []
[dependencies] [dependencies]
bstr = "1.12.0" bstr = "1.12.0"
luaffi_impl = { path = "../luaffi_impl" } luaffi_impl = { path = "../luaffi_impl" }

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
__internal::{display, type_id}, __internal::{display, type_id},
Cdef, CdefBuilder, FfiReturnConvention, IntoFfi, Metatype, MetatypeBuilder, Type, TypeBuilder, Cdef, CdefBuilder, ExternCFn, FfiReturnConvention, IntoFfi, Metatype, MetatypeBuilder, Type,
TypeType, UnsafeExternCFn, TypeBuilder, TypeType,
}; };
use luaify::luaify; use luaify::luaify;
use std::{ use std::{
@ -70,6 +70,45 @@ enum State<F: Future> {
Complete, 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> { impl<F: Future<Output: IntoFfi>> lua_future<F> {
pub fn new(fut: F) -> Self { pub fn new(fut: F) -> Self {
Self { Self {
@ -117,7 +156,12 @@ impl lua_pollable {
pub fn is_valid(&self) -> bool { pub fn is_valid(&self) -> bool {
// TODO: signature check can currently read out-of-bounds if lua code for some reason yields // 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 // 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 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> { unsafe impl<F: Future<Output: IntoFfi> + 'static> IntoFfi for lua_future<F> {
type Into = lua_future<F>; type Into = lua_future<F>;

View File

@ -6,23 +6,6 @@ use std::{
hash::{Hash, Hasher}, 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 { pub fn type_id<T: 'static>() -> u64 {
let mut hash = FxHasher::default(); let mut hash = FxHasher::default();
TypeId::of::<T>().hash(&mut hash); TypeId::of::<T>().hash(&mut hash);

View File

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

View File

@ -3,21 +3,22 @@ use crate::{
string::{DROP_BUFFER_FN, IS_UTF8_FN, lua_buffer}, string::{DROP_BUFFER_FN, IS_UTF8_FN, lua_buffer},
}; };
pub use luaffi_impl::*; pub use luaffi_impl::*;
use rustc_hash::FxHashSet;
use std::{ use std::{
collections::HashSet,
ffi::{c_double, c_float, c_void}, ffi::{c_double, c_float, c_void},
fmt::{self, Display, Formatter, Write}, fmt::{self, Display, Formatter, Write},
marker::PhantomData, marker::PhantomData,
mem, mem,
}; };
pub mod future;
pub mod string;
#[doc(hidden)] #[doc(hidden)]
#[path = "./internal.rs"] #[path = "./internal.rs"]
pub mod __internal; pub mod __internal;
pub mod future;
pub mod marker;
pub mod option;
pub mod result; pub mod result;
pub mod string;
// Dummy function to ensure that strings passed to Rust via wrapper objects will not be // Dummy function to ensure that strings passed to Rust via wrapper objects will not be
// garbage-collected until the end of the function (used in string.rs when string marshalling is // garbage-collected until the end of the function (used in string.rs when string marshalling is
@ -25,8 +26,8 @@ pub mod result;
// `ffi.keep(obj)`. // `ffi.keep(obj)`.
// //
// https://github.com/LuaJIT/LuaJIT/issues/1167 // https://github.com/LuaJIT/LuaJIT/issues/1167
pub(crate) const KEEP_FN: &str = "luaffi_keep"; pub(crate) const KEEP_FN: &str = "__lf_keep";
#[unsafe(export_name = "luaffi_keep")] #[unsafe(export_name = "__lf_keep")]
extern "C" fn __keep(_ptr: *const c_void) {} extern "C" fn __keep(_ptr: *const c_void) {}
export![__keep]; export![__keep];
@ -38,7 +39,7 @@ const CACHE_LIBS: &[(&str, &str)] = &[
("package", "package"), ("package", "package"),
("debug", "debug"), ("debug", "debug"),
("jit", "jit"), ("jit", "jit"),
// require // requires
("bit", r#"require("bit")"#), ("bit", r#"require("bit")"#),
("ffi", r#"require("ffi")"#), ("ffi", r#"require("ffi")"#),
("__tnew", r#"require("table.new")"#), ("__tnew", r#"require("table.new")"#),
@ -47,7 +48,7 @@ const CACHE_LIBS: &[(&str, &str)] = &[
// https://www.lua.org/manual/5.1/manual.html#5.1 // https://www.lua.org/manual/5.1/manual.html#5.1
const CACHE_LOCALS: &[(&str, &str)] = &[ const CACHE_LOCALS: &[(&str, &str)] = &[
// base // baselib
("assert", "assert"), ("assert", "assert"),
("error", "error"), ("error", "error"),
("type", "type"), ("type", "type"),
@ -68,7 +69,8 @@ const CACHE_LOCALS: &[(&str, &str)] = &[
("tonumber", "tonumber"), ("tonumber", "tonumber"),
("tostring", "tostring"), ("tostring", "tostring"),
("require", "require"), ("require", "require"),
// table ("__yield", "coroutine.yield"), // (used in future.rs)
// tablib
("__tconcat", "table.concat"), ("__tconcat", "table.concat"),
("__tinsert", "table.insert"), ("__tinsert", "table.insert"),
("__tmaxn", "table.maxn"), ("__tmaxn", "table.maxn"),
@ -76,23 +78,21 @@ const CACHE_LOCALS: &[(&str, &str)] = &[
("__tsort", "table.sort"), ("__tsort", "table.sort"),
("__tpack", "table.pack"), ("__tpack", "table.pack"),
("__tunpack", "table.unpack"), ("__tunpack", "table.unpack"),
// string // strlib
("__slen", "string.len"), ("__slen", "string.len"),
("__sprintf", "string.format"), ("__sprintf", "string.format"),
("__ssub", "string.sub"), ("__ssub", "string.sub"),
("__sgsub", "string.gsub"), ("__sgsub", "string.gsub"),
("__sgmatch", "string.gmatch"), ("__sgmatch", "string.gmatch"),
("__sdump", "string.dump"), ("__sdump", "string.dump"),
// math (used in luaify! macro) // mathlib (used in luaify! macro)
("__fmod", "math.fmod"), ("__fmod", "math.fmod"),
// coroutine (used in future.rs) // loadlib
("__yield", "coroutine.yield"),
// package
("__preload", "package.preload"), ("__preload", "package.preload"),
// debug // dblib
("__traceback", "debug.traceback"), ("__traceback", "debug.traceback"),
("__registry", "debug.getregistry()"), // (used in lib.lua) ("__registry", "debug.getregistry()"), // (used in lib.lua)
// ffi // ffilib
("__C", "ffi.C"), ("__C", "ffi.C"),
("__ct", "{}"), ("__ct", "{}"),
("__cdef", "ffi.cdef"), ("__cdef", "ffi.cdef"),
@ -105,7 +105,7 @@ const CACHE_LOCALS: &[(&str, &str)] = &[
("__sizeof", "ffi.sizeof"), ("__sizeof", "ffi.sizeof"),
("__alignof", "ffi.alignof"), ("__alignof", "ffi.alignof"),
("__intern", "ffi.string"), // (used in string.rs) ("__intern", "ffi.string"), // (used in string.rs)
// bit (used in luaify! macro) // bitlib (used in luaify! macro)
("__bnot", "bit.bnot"), ("__bnot", "bit.bnot"),
("__band", "bit.band"), ("__band", "bit.band"),
("__bor", "bit.bor"), ("__bor", "bit.bor"),
@ -128,8 +128,8 @@ fn cache_local(f: &mut Formatter, list: &[(&str, &str)]) -> fmt::Result {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Registry { pub struct Registry {
types: HashSet<String>, types: FxHashSet<String>,
funcs: HashSet<String>, decls: FxHashSet<String>,
cdef: String, cdef: String,
lua: String, lua: String,
} }
@ -137,41 +137,41 @@ pub struct Registry {
impl Registry { impl Registry {
pub fn new() -> Self { pub fn new() -> Self {
let mut s = Self::default(); let mut s = Self::default();
s.declare::<UnsafeExternCFn<(*const c_void,), ()>>(KEEP_FN); s.declare::<ExternCFn<(*const c_void,), ()>>(KEEP_FN);
s.declare::<UnsafeExternCFn<(*const u8, usize), bool>>(IS_UTF8_FN); s.declare::<ExternCFn<(*const u8, usize), bool>>(IS_UTF8_FN);
s.declare::<UnsafeExternCFn<(*mut lua_buffer,), ()>>(DROP_BUFFER_FN); s.declare::<ExternCFn<(*mut lua_buffer,), ()>>(DROP_BUFFER_FN);
s s
} }
pub fn include<T: Type>(&mut self) -> &mut Self { pub fn include<T: Type>(&mut self) -> &mut Self {
self.types self.types
.insert(T::name().to_string()) .insert(T::name().to_string())
.then(|| T::build(&mut TypeBuilder::new::<T>(self))); .then(|| T::build(&mut TypeBuilder::new(self)));
self self
} }
pub fn declare<T: Type>(&mut self, name: impl Display) -> &mut Self { pub fn declare<T: Type>(&mut self, name: impl Display) -> &mut Self {
assert!(T::ty() != TypeType::Void, "cannot declare void type"); assert!(T::ty() != TypeType::Void, "cannot declare void type");
self.include::<T>() self.include::<T>()
.funcs .decls
.insert(name.to_string()) .insert(T::extern_cdecl(&name).to_string())
.then(|| writeln!(self.cdef, "{};", T::extern_cdecl(name)).unwrap()); .then(|| writeln!(self.cdef, "{};", T::extern_cdecl(&name)).unwrap());
self self
} }
pub fn preload<T: Type>(&mut self, name: impl Display) -> &mut Self { pub fn preload<T: Module>(&mut self) -> &mut Self {
assert!(T::ty() != TypeType::Void, "cannot declare void type"); assert!(T::ty() != TypeType::Void, "cannot declare void type");
self.include::<T>(); let name = <T as Module>::name();
let ct = T::name(); let ct = <T as Type>::name();
writeln!( writeln!(
self.lua, self.lua,
r#"__preload["{name}"] = function(...) return __ct.{ct}(...); end;"#, r#"__preload["{name}"] = function(...) return __ct.{ct}(...); end;"#,
) )
.unwrap(); .unwrap();
self self.include::<T>()
} }
pub fn done(&self) -> String { pub fn build(&self) -> String {
self.to_string() self.to_string()
} }
} }
@ -184,7 +184,7 @@ impl Display for Registry {
cache_local(f, CACHE_LIBS)?; cache_local(f, CACHE_LIBS)?;
cache_local(f, CACHE_LOCALS)?; cache_local(f, CACHE_LOCALS)?;
writeln!(f, "{}", include_str!("./lib.lua"))?; writeln!(f, "{}", include_str!("./lib.lua"))?;
writeln!(f, "__cdef [[\n{}\n]];", self.cdef.trim())?; writeln!(f, "__cdef [[\n{}\n]];", self.cdef.trim_end())?;
write!(f, "{}", self.lua) write!(f, "{}", self.lua)
} }
} }
@ -210,53 +210,49 @@ pub enum TypeType {
#[derive(Debug)] #[derive(Debug)]
pub struct TypeBuilder<'r> { pub struct TypeBuilder<'r> {
registry: &'r mut Registry, reg: &'r mut Registry,
} }
impl<'r> TypeBuilder<'r> { impl<'r> TypeBuilder<'r> {
fn new<T: Type>(registry: &'r mut Registry) -> Self { fn new(reg: &'r mut Registry) -> Self {
let ct = T::name(); Self { reg }
let cdecl = T::cdecl("");
writeln!(registry.lua, r#"__ct.{ct} = __typeof("{cdecl}");"#).unwrap();
Self { registry }
}
pub fn include<T: Type>(&mut self) -> &mut Self {
self.registry.include::<T>();
self
} }
pub fn cdef<T: Cdef>(&mut self) -> &mut Self { pub fn cdef<T: Cdef>(&mut self) -> &mut Self {
let mut b = CdefBuilder::new::<T>(self.registry); let mut b = CdefBuilder::new::<T>(self.reg);
<T as Cdef>::build(&mut b); <T as Cdef>::build(&mut b);
drop(b); drop(b);
self self
} }
pub fn metatype<T: Metatype>(&mut self) -> &mut Self { pub fn metatype<T: Metatype>(&mut self) -> &mut Self {
let mut b = MetatypeBuilder::new::<T>(self.registry); let mut b = MetatypeBuilder::new::<T>(self.reg);
<T as Metatype>::build(&mut b); <T as Metatype>::build(&mut b);
drop(b); drop(b);
self self
} }
} }
pub trait Module: Type {
fn name() -> impl Display;
}
pub unsafe trait Cdef: Type { pub unsafe trait Cdef: Type {
fn build(b: &mut CdefBuilder); fn build(b: &mut CdefBuilder);
} }
#[derive(Debug)] #[derive(Debug)]
pub struct CdefBuilder<'r> { pub struct CdefBuilder<'r> {
registry: &'r mut Registry, reg: &'r mut Registry,
cdef: String, cdef: String,
align: usize, align: usize,
opaque: usize, opaque: usize,
} }
impl<'r> CdefBuilder<'r> { impl<'r> CdefBuilder<'r> {
fn new<T: Cdef>(registry: &'r mut Registry) -> Self { fn new<T: Cdef>(reg: &'r mut Registry) -> Self {
Self { Self {
registry, reg,
cdef: format!("{} {{ ", T::cdecl("")), cdef: format!("{} {{ ", T::cdecl("")),
align: mem::align_of::<T>(), align: mem::align_of::<T>(),
opaque: 0, opaque: 0,
@ -265,7 +261,7 @@ impl<'r> CdefBuilder<'r> {
pub fn field<T: Type>(&mut self, name: impl Display) -> &mut Self { pub fn field<T: Type>(&mut self, name: impl Display) -> &mut Self {
assert!(T::ty() != TypeType::Void, "cannot declare void field"); assert!(T::ty() != TypeType::Void, "cannot declare void field");
self.registry.include::<T>(); self.reg.include::<T>();
self.field_raw(T::cdecl(name)) self.field_raw(T::cdecl(name))
} }
@ -299,14 +295,11 @@ impl<'r> CdefBuilder<'r> {
impl<'r> Drop for CdefBuilder<'r> { impl<'r> Drop for CdefBuilder<'r> {
fn drop(&mut self) { fn drop(&mut self) {
let Self { let Self {
registry, reg, cdef, align, ..
cdef,
align,
..
} = self; } = self;
registry.cdef.push_str(cdef); reg.cdef.push_str(cdef);
writeln!(registry.cdef, "}} __attribute__((aligned({align})));").unwrap(); writeln!(reg.cdef, "}} __attribute__((aligned({align})));").unwrap();
} }
} }
@ -317,56 +310,73 @@ pub unsafe trait Metatype {
#[derive(Debug)] #[derive(Debug)]
pub struct MetatypeBuilder<'r> { pub struct MetatypeBuilder<'r> {
registry: &'r mut Registry, reg: &'r mut Registry,
ct: String, ct: String,
cdef: String, cdef: String,
lua: String, lua: String,
lua_includes: Vec<&'static str>,
has_index: bool,
} }
impl<'r> MetatypeBuilder<'r> { impl<'r> MetatypeBuilder<'r> {
fn new<T: Metatype>(registry: &'r mut Registry) -> Self { fn new<T: Metatype>(reg: &'r mut Registry) -> Self {
// NOTE: this needs to be written first, because recursively included dependency types might
// need it
let ct = T::Target::name();
let cdecl = T::Target::cdecl("");
writeln!(reg.lua, r#"__ct.{ct} = __typeof("{cdecl}");"#).unwrap();
Self { Self {
registry, reg,
ct: T::Target::name().to_string(), ct: T::Target::name().to_string(),
cdef: String::new(), cdef: String::new(),
lua: r#"do local __mt, __idx = {}, {}; __mt.__index = __idx; "#.into(), lua: String::new(),
lua_includes: vec![],
has_index: false,
} }
} }
pub fn declare<T: Type>(&mut self, name: impl Display) -> &mut Self { pub fn declare<T: Type>(&mut self, name: impl Display) -> &mut Self {
self.registry.declare::<T>(name); self.reg.declare::<T>(name);
self
}
pub fn include_lua(&mut self, lua: &'static str) -> &mut Self {
self.lua_includes.push(lua);
self self
} }
pub fn index( pub fn index(
&mut self, &mut self,
name: impl Display, name: impl Display,
f: impl FnOnce(&mut MetatypeMethodBuilder), f: impl FnOnce(&mut MetatypeFunctionBuilder),
) -> &mut Self { ) -> &mut Self {
write!(self.lua, "__idx.{name} = ").unwrap(); write!(self.lua, "Self.{name} = ").unwrap();
f(&mut MetatypeMethodBuilder::new(self)); f(&mut MetatypeFunctionBuilder::new(self));
write!(self.lua, "; ").unwrap(); writeln!(self.lua, ";").unwrap();
self.has_index = true;
self self
} }
pub fn index_raw(&mut self, name: impl Display, value: impl Display) -> &mut Self { pub fn index_raw(&mut self, name: impl Display, value: impl Display) -> &mut Self {
write!(self.lua, "__idx.{name} = {value}; ").unwrap(); writeln!(self.lua, "Self.{name} = {value};").unwrap();
self.has_index = true;
self self
} }
pub fn metatable( pub fn metatable(
&mut self, &mut self,
name: impl Display, name: impl Display,
f: impl FnOnce(&mut MetatypeMethodBuilder), f: impl FnOnce(&mut MetatypeFunctionBuilder),
) -> &mut Self { ) -> &mut Self {
write!(self.lua, "__mt.__{name} = ").unwrap(); write!(self.lua, "__mt.__{name} = ").unwrap();
f(&mut MetatypeMethodBuilder::new(self)); f(&mut MetatypeFunctionBuilder::new(self));
write!(self.lua, "; ").unwrap(); writeln!(self.lua, ";").unwrap();
self self
} }
pub fn metatable_raw(&mut self, name: impl Display, value: impl Display) -> &mut Self { pub fn metatable_raw(&mut self, name: impl Display, value: impl Display) -> &mut Self {
write!(self.lua, "__mt.__{name} = {value}; ").unwrap(); writeln!(self.lua, "__mt.__{name} = {value};").unwrap();
self self
} }
} }
@ -374,16 +384,28 @@ impl<'r> MetatypeBuilder<'r> {
impl<'r> Drop for MetatypeBuilder<'r> { impl<'r> Drop for MetatypeBuilder<'r> {
fn drop(&mut self) { fn drop(&mut self) {
let Self { let Self {
registry, reg,
ct, ct,
cdef, cdef,
lua, lua,
.. lua_includes,
has_index,
} = self; } = self;
registry.cdef.push_str(cdef); write!(reg.lua, r#"do local __mt = {{}}; "#).unwrap();
registry.lua.push_str(lua);
writeln!(registry.lua, r#"__metatype(__ct.{ct}, __mt); end;"#).unwrap(); if *has_index {
write!(reg.lua, r#"local Self = {{}}; __mt.__index = Self; "#).unwrap();
}
reg.cdef.push_str(cdef);
reg.lua.push_str(lua.trim_end());
writeln!(reg.lua, r#" __metatype(__ct.{ct}, __mt); end;"#).unwrap();
for lua in lua_includes {
writeln!(reg.lua, "do {}\nend;", lua.trim_end()).unwrap();
}
} }
} }
@ -429,16 +451,17 @@ pub unsafe trait IntoFfi: Sized {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct MetatypeMethodBuilder<'r, 'm> { pub struct MetatypeFunctionBuilder<'r, 'm> {
metatype: &'m mut MetatypeBuilder<'r>, metatype: &'m mut MetatypeBuilder<'r>,
lparams: String, // parameters to the lua function lparams: String, // lua function parameters
cparams: String, // parameters to the lua function cparams: String, // C function parameters
cargs: String, // arguments to the C call cargs: String, // C call arguments
prelude: String, // function body prelude prelude: String, // lua function body prelude
postlude: String, // function body postlude postlude: String, // lua function body postlude
is_eqmm: bool,
} }
impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> { impl<'r, 'm> MetatypeFunctionBuilder<'r, 'm> {
pub fn new(metatype: &'m mut MetatypeBuilder<'r>) -> Self { pub fn new(metatype: &'m mut MetatypeBuilder<'r>) -> Self {
Self { Self {
metatype, metatype,
@ -447,17 +470,18 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
cargs: String::new(), cargs: String::new(),
prelude: String::new(), prelude: String::new(),
postlude: String::new(), postlude: String::new(),
is_eqmm: false,
} }
} }
pub fn set_eqmm(&mut self, eq: bool) -> &mut Self {
self.is_eqmm = eq; // are we building an __eq metamethod?
self
}
pub fn param<T: FromFfi>(&mut self, name: impl Display) -> &mut Self { pub fn param<T: FromFfi>(&mut self, name: impl Display) -> &mut Self {
assert!(
T::From::ty() != TypeType::Void,
"cannot declare void parameter"
);
let Self { let Self {
metatype: MetatypeBuilder { registry, .. }, metatype: MetatypeBuilder { reg, .. },
lparams, lparams,
cparams, cparams,
cargs, cargs,
@ -466,7 +490,15 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
.. ..
} = self; } = self;
registry.include::<T::From>(); assert!(
T::From::ty() != TypeType::Void,
"cannot declare void parameter"
);
// should already be prevented by #[metatype] macro
debug_assert!(!name.to_string().starts_with("__"));
reg.include::<T::From>();
(!lparams.is_empty()).then(|| lparams.push_str(", ")); (!lparams.is_empty()).then(|| lparams.push_str(", "));
(!cparams.is_empty()).then(|| cparams.push_str(", ")); (!cparams.is_empty()).then(|| cparams.push_str(", "));
@ -485,6 +517,26 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
self self
} }
pub fn param_ctchecked<T: FromFfi, U: Type + Metatype<Target = U>>(
&mut self,
name: impl Display,
) -> &mut Self {
let ct = U::name();
if self.is_eqmm {
// special case for building __eq metamethods which are expected to return false if the
// arguments aren't of the expected type, because otherwise the `==` and `~=` operators
// could throw instead of returning false.
write!(
self.prelude,
r#"if not __istype(__ct.{ct}, {name}) then return false; end; "#
)
} else {
write!(self.prelude, r#"assert(__istype(__ct.{ct}, {name})); "#)
}
.unwrap();
self.param::<T>(name)
}
pub fn param_str( pub fn param_str(
&mut self, &mut self,
name: impl Display, name: impl Display,
@ -495,7 +547,9 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
// //
// this passes one lua `string` argument as two C `const uint8_t *ptr` and `uintptr_t len` // this passes one lua `string` argument as two C `const uint8_t *ptr` and `uintptr_t len`
// arguments, bypassing the slower generic `&[u8]: FromFfi` path which constructs a // arguments, bypassing the slower generic `&[u8]: FromFfi` path which constructs a
// temporary cdata to pass the string and its length in one argument. // temporary cdata to pass the string and its length in one argument. this is used when the
// #[metatype] macro detects a string-like parameter, currently defined to be &[u8], &str,
// &BStr (from the bstr crate), or those types wrapped in Option<T>.
let Self { let Self {
lparams, lparams,
cparams, cparams,
@ -504,6 +558,9 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
.. ..
} = self; } = self;
// should already be prevented by #[metatype] macro
debug_assert!(!name.to_string().starts_with("__"));
let param_ptr = <*const u8>::cdecl(&name); let param_ptr = <*const u8>::cdecl(&name);
let param_len = usize::cdecl(format!("{name}_len")); let param_len = usize::cdecl(format!("{name}_len"));
@ -514,15 +571,22 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
write!(lparams, "{name}").unwrap(); write!(lparams, "{name}").unwrap();
write!(cparams, "{param_ptr}, {param_len}").unwrap(); write!(cparams, "{param_ptr}, {param_len}").unwrap();
write!(cargs, "{name}, __{name}_len").unwrap(); write!(cargs, "{name}, __{name}_len").unwrap();
write!(prelude, "local __{name}_len = 0; if {name} ~= nil then ").unwrap(); write!(
prelude,
"local __{name}_len = 0; if not rawequal({name}, nil) then "
)
.unwrap();
write!(prelude, r#"assert(type({name}) == "string", "string expected in argument '{name}', got " .. type({name})); "#).unwrap(); write!(prelude, r#"assert(type({name}) == "string", "string expected in argument '{name}', got " .. type({name})); "#).unwrap();
write!(prelude, r#"__{name}_len = #{name}; "#).unwrap(); write!(prelude, r#"__{name}_len = #{name}; "#).unwrap();
if check_utf8 { if check_utf8 {
write!(prelude, r#"assert(__C.{IS_UTF8_FN}({name}, __{name}_len), "argument '{name}' must be a valid utf-8 string"); "#).unwrap(); write!(prelude, r#"assert(__C.{IS_UTF8_FN}({name}, __{name}_len), "argument '{name}' must be a valid utf-8 string"); "#).unwrap();
} }
if !allow_nil { if !allow_nil {
write!(prelude, r#"else return error("string expected in argument '{name}', got " .. type({name})); "#).unwrap(); write!(prelude, r#"else return error("string expected in argument '{name}', got " .. type({name})); "#).unwrap();
} }
write!(prelude, r#"end; "#).unwrap(); write!(prelude, r#"end; "#).unwrap();
self self
} }
@ -535,13 +599,7 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
pub fn call<T: IntoFfi>(&mut self, func: impl Display) { pub fn call<T: IntoFfi>(&mut self, func: impl Display) {
let Self { let Self {
metatype: metatype: MetatypeBuilder { reg, cdef, lua, .. },
MetatypeBuilder {
registry,
cdef,
lua,
..
},
lparams, lparams,
cparams, cparams,
cargs, cargs,
@ -550,7 +608,7 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
.. ..
} = self; } = self;
registry.include::<T::Into>(); reg.include::<T::Into>();
write!(lua, "function({lparams}) {prelude}").unwrap(); write!(lua, "function({lparams}) {prelude}").unwrap();
match T::convention() { match T::convention() {
@ -558,22 +616,22 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
if T::Into::ty() == TypeType::Void { if T::Into::ty() == TypeType::Void {
write!(lua, "__C.{func}({cargs}); {postlude}end").unwrap(); write!(lua, "__C.{func}({cargs}); {postlude}end").unwrap();
} else { } else {
let check = T::postlude("__ret"); let check = T::postlude("ret");
write!(lua, "local __ret = __C.{func}({cargs}); ").unwrap(); write!(lua, "local ret = __C.{func}({cargs}); ").unwrap();
write!(lua, "{check}{postlude}return __ret; end").unwrap(); write!(lua, "{check}{postlude}return ret; end").unwrap();
} }
writeln!(cdef, "{};", T::Into::cdecl(display!("{func}({cparams})"))).unwrap(); writeln!(cdef, "{};", T::Into::cdecl(display!("{func}({cparams})"))).unwrap();
} }
FfiReturnConvention::ByOutParam => { FfiReturnConvention::ByOutParam => {
let ct = T::Into::name(); let ct = T::Into::name();
let check = T::postlude("__out"); let check = T::postlude("__ret");
write!(lua, "local __out = __new(__ct.{ct}); __C.{func}(__out").unwrap(); write!(lua, "local __ret = __new(__ct.{ct}); __C.{func}(__ret").unwrap();
if !cargs.is_empty() { if !cargs.is_empty() {
write!(lua, ", {cargs}").unwrap(); write!(lua, ", {cargs}").unwrap();
} }
write!(lua, "); {check}{postlude}return __out; end").unwrap(); write!(lua, "); {check}{postlude}return __ret; end").unwrap();
write!(cdef, "void {func}({}", <*mut T::Into>::cdecl("out")).unwrap(); write!(cdef, "void {func}({}", <*mut T::Into>::cdecl("__out")).unwrap();
if !cparams.is_empty() { if !cparams.is_empty() {
write!(cdef, ", {cparams}").unwrap(); write!(cdef, ", {cparams}").unwrap();
} }
@ -587,6 +645,10 @@ impl<'r, 'm> MetatypeMethodBuilder<'r, 'm> {
} }
} }
pub trait Annotate {
fn annotation() -> impl Display;
}
// //
// SAFETY: Unit type return maps to a C void return, which is a nil return in lua. There is no // SAFETY: Unit type return maps to a C void return, which is a nil return in lua. There is no
// equivalent to passing a unit type as an argument in C. // equivalent to passing a unit type as an argument in C.
@ -620,7 +682,7 @@ impl_void!(());
impl_void!(c_void); impl_void!(c_void);
macro_rules! impl_primitive { macro_rules! impl_primitive {
($rty:ty, $cty:expr) => { ($rty:ty, $cty:expr, $lty:expr) => {
unsafe impl Type for $rty { unsafe impl Type for $rty {
fn name() -> impl Display { fn name() -> impl Display {
$cty $cty
@ -636,22 +698,28 @@ macro_rules! impl_primitive {
fn build(_b: &mut TypeBuilder) {} fn build(_b: &mut TypeBuilder) {}
} }
impl Annotate for $rty {
fn annotation() -> impl Display {
$lty
}
}
}; };
} }
impl_primitive!(bool, "bool"); impl_primitive!(bool, "bool", "boolean");
impl_primitive!(u8, "uint8_t"); impl_primitive!(u8, "uint8_t", "number");
impl_primitive!(u16, "uint16_t"); impl_primitive!(u16, "uint16_t", "number");
impl_primitive!(u32, "uint32_t"); impl_primitive!(u32, "uint32_t", "number");
impl_primitive!(u64, "uint64_t"); impl_primitive!(u64, "uint64_t", "number");
impl_primitive!(usize, "uintptr_t"); impl_primitive!(usize, "uintptr_t", "number");
impl_primitive!(i8, "int8_t"); impl_primitive!(i8, "int8_t", "number");
impl_primitive!(i16, "int16_t"); impl_primitive!(i16, "int16_t", "number");
impl_primitive!(i32, "int32_t"); impl_primitive!(i32, "int32_t", "number");
impl_primitive!(i64, "int64_t"); impl_primitive!(i64, "int64_t", "number");
impl_primitive!(isize, "intptr_t"); impl_primitive!(isize, "intptr_t", "number");
impl_primitive!(c_float, "float"); impl_primitive!(c_float, "float", "number");
impl_primitive!(c_double, "double"); impl_primitive!(c_double, "double", "number");
unsafe impl FromFfi for bool { unsafe impl FromFfi for bool {
type From = bool; type From = bool;
@ -741,8 +809,7 @@ macro_rules! impl_bigint_intoabi {
fn postlude(ret: &str) -> impl Display { fn postlude(ret: &str) -> impl Display {
// this isn't "correct" per se, but it's much more ergonomic to work with numbers in // this isn't "correct" per se, but it's much more ergonomic to work with numbers in
// lua than with long longs wrapped in cdata. we gracefully accept the loss of // lua than with long longs wrapped in cdata. we gracefully accept the loss of
// precision here and that 53 bits of precision for big integers are enough. (the // precision here and that 53 bits of precision for big integers are enough.
// vain of Lua 5.3 integer subtype ;D )
display!("{ret} = tonumber({ret}); ") display!("{ret} = tonumber({ret}); ")
} }
} }
@ -756,11 +823,19 @@ impl_bigint_intoabi!(usize);
#[cfg(target_pointer_width = "64")] #[cfg(target_pointer_width = "64")]
impl_bigint_intoabi!(isize); impl_bigint_intoabi!(isize);
macro_rules! impl_const_ptr { macro_rules! impl_ptr {
($ty:ty) => { ($ty:ty, $mutable:expr) => {
unsafe impl<T: Type> Type for $ty { unsafe impl<T> Type for $ty
where
T: Type,
{
fn name() -> impl Display { fn name() -> impl Display {
display!("const_{}_ptr", T::name()) disp(|f| {
if !$mutable {
write!(f, "const_")?;
}
write!(f, "{}_ptr", T::name())
})
} }
fn ty() -> TypeType { fn ty() -> TypeType {
@ -768,45 +843,43 @@ macro_rules! impl_const_ptr {
} }
fn cdecl(name: impl Display) -> impl Display { fn cdecl(name: impl Display) -> impl Display {
T::cdecl(display!("const *{name}")) T::cdecl(disp(move |f| {
if !$mutable {
write!(f, "const ")?;
}
write!(f, "*{name}")
}))
} }
fn build(b: &mut TypeBuilder) { fn build(b: &mut TypeBuilder) {
b.include::<T>(); b.reg.include::<T>();
} }
} }
}; };
} }
impl_const_ptr!(*const T); impl_ptr!(&T, false);
impl_const_ptr!(&T); impl_ptr!(&mut T, true);
impl_const_ptr!(Option<&T>); impl_ptr!(*const T, false);
impl_ptr!(*mut T, true);
impl_ptr!(Option<&T>, false);
impl_ptr!(Option<&mut T>, true);
macro_rules! impl_mut_ptr { macro_rules! impl_ref_annotation {
($ty:ty) => { ($ty:ty) => {
unsafe impl<T: Type> Type for $ty { impl<T> Annotate for $ty
fn name() -> impl Display { where
display!("{}_ptr", T::name()) T: Annotate,
} {
fn annotation() -> impl Display {
fn ty() -> TypeType { display!("{}", T::annotation())
TypeType::Primitive
}
fn cdecl(name: impl Display) -> impl Display {
T::cdecl(display!("*{name}"))
}
fn build(b: &mut TypeBuilder) {
b.include::<T>();
} }
} }
}; };
} }
impl_mut_ptr!(*mut T); impl_ref_annotation!(&T);
impl_mut_ptr!(&mut T); impl_ref_annotation!(&mut T);
impl_mut_ptr!(Option<&mut T>);
// //
// SAFETY: Pass by value for pointers, which maps to a `cdata` argument in lua containing either: // SAFETY: Pass by value for pointers, which maps to a `cdata` argument in lua containing either:
@ -819,7 +892,10 @@ impl_mut_ptr!(Option<&mut T>);
// //
macro_rules! impl_ptr_fromabi { macro_rules! impl_ptr_fromabi {
($ty:ty) => { ($ty:ty) => {
unsafe impl<T: Type> FromFfi for $ty { unsafe impl<T> FromFfi for $ty
where
T: Type,
{
type From = Self; type From = Self;
fn convert(from: Self::From) -> Self { fn convert(from: Self::From) -> Self {
@ -841,7 +917,10 @@ impl_ptr_fromabi!(Option<&mut T>);
// //
macro_rules! impl_ptr_intoabi { macro_rules! impl_ptr_intoabi {
($ty:ty) => { ($ty:ty) => {
unsafe impl<T: Type> IntoFfi for $ty { unsafe impl<T> IntoFfi for $ty
where
T: Type,
{
type Into = Self; type Into = Self;
fn convert(self) -> Self::Into { fn convert(self) -> Self::Into {
@ -857,34 +936,19 @@ macro_rules! impl_ptr_intoabi {
impl_ptr_intoabi!(*const T); impl_ptr_intoabi!(*const T);
impl_ptr_intoabi!(*mut T); impl_ptr_intoabi!(*mut T);
#[cfg(feature = "option_ref_abi")] // disabled because it conflicts with the generic Option<T> impl
impl_ptr_intoabi!(Option<&'static T>); impl_ptr_intoabi!(Option<&'static T>);
#[cfg(feature = "option_ref_abi")]
impl_ptr_intoabi!(Option<&'static mut T>); impl_ptr_intoabi!(Option<&'static mut T>);
// unsafe impl<'s, T> FromFfi for &'s T
// SAFETY: `FromFfi` for *mutable* references is safe because it is guaranteed that no two Rust code where
// called via FFI can be running at the same time on the same OS thread (no Lua reentrancy). T: Type,
// {
// i.e. The call stack will always look something like this: type From = Option<&'s T>;
//
// * Runtime (LuaJIT/Rust) -> Lua (via C) -> Rust (via FFI): This is SAFE and the only use case we
// support. All references (mutable or not) to Rust user objects will be dropped before
// returning to Lua.
//
// * Runtime (LuaJIT/Rust) -> Lua (via C) -> Rust (via FFI) -> Lua (via callback): This is UNSAFE
// because we cannot prevent the Lua callback from calling back into Rust code via FFI which
// could violate exclusive borrow semantics. This is prevented by not implementing `FromFfi` for
// function pointers (see below).
//
// The runtime does not keep any references to Rust user objects boxed in cdata (futures are the
// only exception; their ownership is transferred to the runtime via yield).
//
macro_rules! impl_ref_fromabi {
($ty:ty) => {
unsafe impl<'s, T: Type> FromFfi for $ty {
type From = Option<$ty>;
fn prelude(arg: &str) -> impl Display { fn prelude(arg: &str) -> impl Display {
display!(r#"assert({arg} ~= nil, "argument '{arg}' cannot be nil"); "#) display!(r#"assert(not rawequal({arg}, nil), "argument '{arg}' cannot be nil"); "#)
} }
fn convert(from: Self::From) -> Self { fn convert(from: Self::From) -> Self {
@ -897,39 +961,33 @@ macro_rules! impl_ref_fromabi {
unsafe { from.unwrap_unchecked() } unsafe { from.unwrap_unchecked() }
} }
}
};
} }
impl_ref_fromabi!(&'s T);
impl_ref_fromabi!(&'s mut T);
// //
// SAFETY: `IntoFfi` only for 'static references because we cannot guarantee that the pointer will // SAFETY: `IntoFfi` only for 'static references because we cannot guarantee that the pointer will
// not outlive the pointee otherwise. // not outlive the pointee otherwise.
// //
macro_rules! impl_ref_intoabi { unsafe impl<T> IntoFfi for &'static T
($ty:ty) => { where
unsafe impl<T: Type> IntoFfi for $ty { T: Type,
{
type Into = Self; type Into = Self;
fn convert(self) -> Self::Into { fn convert(self) -> Self::Into {
self self
} }
}
};
} }
impl_ref_intoabi!(&'static T);
impl_ref_intoabi!(&'static mut T);
// //
// SAFETY: No `FromFfi` and `IntoFfi` for arrays because passing or returning them by value is not a // SAFETY: No `FromFfi` and `IntoFfi` for arrays because passing or returning them by value is not a
// thing in C (they are just pointers). // thing in C (they are just pointers).
// //
// TODO: we could automatically convert them to tables and vice-versa // TODO: we could automatically convert them to tables and vice-versa
// //
unsafe impl<T: Type> Type for [T] { unsafe impl<T> Type for [T]
where
T: Type,
{
fn name() -> impl Display { fn name() -> impl Display {
display!("{}_arr", T::name()) display!("{}_arr", T::name())
} }
@ -943,11 +1001,23 @@ unsafe impl<T: Type> Type for [T] {
} }
fn build(b: &mut TypeBuilder) { fn build(b: &mut TypeBuilder) {
b.include::<T>(); b.reg.include::<T>();
} }
} }
unsafe impl<T: Type, const N: usize> Type for [T; N] { impl<T> Annotate for [T]
where
T: Annotate,
{
fn annotation() -> impl Display {
display!("{}[]", T::annotation())
}
}
unsafe impl<T, const N: usize> Type for [T; N]
where
T: Type,
{
fn name() -> impl Display { fn name() -> impl Display {
display!("{}_arr{N}", T::name()) display!("{}_arr{N}", T::name())
} }
@ -961,26 +1031,32 @@ unsafe impl<T: Type, const N: usize> Type for [T; N] {
} }
fn build(b: &mut TypeBuilder) { fn build(b: &mut TypeBuilder) {
b.include::<T>(); b.reg.include::<T>();
} }
} }
pub struct UnsafeExternCFn<In, Out>(PhantomData<unsafe extern "C" fn(In) -> Out>); impl<T, const N: usize> Annotate for [T; N]
where
T: Annotate,
{
fn annotation() -> impl Display {
display!("{}[]", T::annotation())
}
}
macro_rules! impl_function { pub struct ExternCFn<I, O>(PhantomData<extern "C" fn(I) -> O>);
(fn($($arg:tt),*) -> $ret:tt) => {
impl_function!(UnsafeExternCFn, fn($($arg),*) -> $ret); macro_rules! impl_externcfn {
(fn($($arg:ident),*) -> $ret:ident) => {
impl_externcfn!(ExternCFn, fn($($arg),*) -> $ret);
}; };
($ty:tt, fn($($arg:tt),*) -> $ret:tt) => { ($ty:ident, fn($($arg:ident),*) -> $ret:ident) => {
// unsafe impl<$($arg,)* $ret> Type for $ty<($($arg,)*), $ret>
// SAFETY: No `FromFfi` for function pointers because of borrow safety invariants (see above where
// in `&mut T`). $($arg: Type,)*
// $ret: Type,
// We also can't implement `IntoFfi` because we can't call `FromFfi` and `IntoFfi` for the {
// function's respective argument and return values.
//
unsafe impl<$($arg: Type,)* $ret: Type> Type for $ty<($($arg,)*), $ret> {
fn name() -> impl Display { fn name() -> impl Display {
disp(|f| Ok({ disp(|f| Ok({
write!(f, "fn_{}", $ret::name())?; write!(f, "fn_{}", $ret::name())?;
@ -994,8 +1070,8 @@ macro_rules! impl_function {
fn cdecl(name: impl Display) -> impl Display { fn cdecl(name: impl Display) -> impl Display {
$ret::cdecl(disp(move |f| Ok({ $ret::cdecl(disp(move |f| Ok({
let mut _n = 0;
write!(f, "(*{name})(")?; write!(f, "(*{name})(")?;
let mut _n = 0;
$(if _n != 0 { write!(f, ", ")?; } write!(f, "{}", $arg::cdecl(""))?; _n += 1;)* $(if _n != 0 { write!(f, ", ")?; } write!(f, "{}", $arg::cdecl(""))?; _n += 1;)*
write!(f, ")")?; write!(f, ")")?;
}))) })))
@ -1004,26 +1080,52 @@ macro_rules! impl_function {
fn extern_cdecl(name: impl Display) -> impl Display { fn extern_cdecl(name: impl Display) -> impl Display {
$ret::cdecl(disp(move |f| Ok({ $ret::cdecl(disp(move |f| Ok({
// for top-level function declarations in cdef // for top-level function declarations in cdef
let mut _n = 0;
write!(f, "{name}(")?; write!(f, "{name}(")?;
let mut _n = 0;
$(if _n != 0 { write!(f, ", ")?; } write!(f, "{}", $arg::cdecl(""))?; _n += 1;)* $(if _n != 0 { write!(f, ", ")?; } write!(f, "{}", $arg::cdecl(""))?; _n += 1;)*
write!(f, ")")?; write!(f, ")")?;
}))) })))
} }
fn build(b: &mut TypeBuilder) { fn build(b: &mut TypeBuilder) {
$(b.include::<$arg>();)* $(b.reg.include::<$arg>();)*
b.include::<$ret>(); b.reg.include::<$ret>();
} }
} }
}; };
} }
impl_function!(fn() -> Z); impl_externcfn!(fn() -> A);
impl_function!(fn(A) -> Z); impl_externcfn!(fn(A) -> B);
impl_function!(fn(A, B) -> Z); impl_externcfn!(fn(A, B) -> C);
impl_function!(fn(A, B, C) -> Z); impl_externcfn!(fn(A, B, C) -> D);
impl_function!(fn(A, B, C, D) -> Z); impl_externcfn!(fn(A, B, C, D) -> E);
impl_function!(fn(A, B, C, D, E) -> Z); impl_externcfn!(fn(A, B, C, D, E) -> F);
impl_function!(fn(A, B, C, D, E, F) -> Z); impl_externcfn!(fn(A, B, C, D, E, F) -> G);
impl_function!(fn(A, B, C, D, E, F, G) -> Z); impl_externcfn!(fn(A, B, C, D, E, F, G) -> H);
impl_externcfn!(fn(A, B, C, D, E, F, G, H) -> I);
impl_externcfn!(fn(A, B, C, D, E, F, G, H, I) -> J);
impl<'s> Annotate for &'s [u8] {
fn annotation() -> impl Display {
"string"
}
}
impl<'s> Annotate for &'s str {
fn annotation() -> impl Display {
"string"
}
}
impl Annotate for Vec<u8> {
fn annotation() -> impl Display {
"string"
}
}
impl Annotate for String {
fn annotation() -> impl Display {
"string"
}
}

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::{ use crate::{
__internal::{disp, display}, __internal::{disp, display, type_id},
Cdef, CdefBuilder, IntoFfi, KEEP_FN, Type, TypeBuilder, TypeType, Cdef, CdefBuilder, IntoFfi, KEEP_FN, Metatype, MetatypeBuilder, Type, TypeBuilder, TypeType,
string::{DROP_BUFFER_FN, lua_buffer}, string::{DROP_BUFFER_FN, lua_buffer},
}; };
use std::{ffi::c_int, fmt::Display}; use std::{ffi::c_int, fmt::Display};
@ -12,9 +12,9 @@ pub enum lua_result<T> {
Ok(T), // __tag = 1 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 { fn name() -> impl Display {
display!("result__{}", T::name()) display!("__result_{:x}", type_id::<T>())
} }
fn ty() -> TypeType { fn ty() -> TypeType {
@ -26,11 +26,11 @@ unsafe impl<T: Type> Type for lua_result<T> {
} }
fn build(b: &mut TypeBuilder) { 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) { fn build(b: &mut CdefBuilder) {
b.field::<c_int>("__tag").inner_union(|b| { b.field::<c_int>("__tag").inner_union(|b| {
(T::ty() != TypeType::Void).then(|| b.field::<T>("__value")); (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>; type Into = lua_result<T::Into>;
fn convert(self) -> Self::Into { fn convert(self) -> Self::Into {
@ -72,12 +77,12 @@ unsafe impl<T: IntoFfi, E: Display> IntoFfi for Result<T, E> {
write!(f, "{ret} = __new(__ct.{ct}, {ret}.__value); ")?; write!(f, "{ret} = __new(__ct.{ct}, {ret}.__value); ")?;
write!(f, "{}", T::postlude(ret))?; write!(f, "{}", T::postlude(ret))?;
} else { } else {
// inner value is a "temporary" itself and doesn't require full ownership of // inner value is a "temporary" like a result itself and doesn't require
// itself. we just need to keep the result object alive until its postlude // full ownership of itself. we just need to keep the result object alive
// completes. // until its postlude completes.
write!(f, "local __{ret} = {ret}; {ret} = {ret}.__value; ")?; write!(f, "local {ret}_keep = {ret}; {ret} = {ret}.__value; ")?;
write!(f, "do {}end; ", T::postlude(ret))?; write!(f, "do {}end; ", T::postlude(ret))?;
write!(f, "__C.{KEEP_FN}(__{ret}); ")?; // keep original result alive write!(f, "__C.{KEEP_FN}({ret}_keep); ")?; // keep original result alive
} }
} }
} }

View File

@ -4,18 +4,18 @@ use crate::{
}; };
use bstr::{BStr, BString}; use bstr::{BStr, BString};
use luaffi_impl::{cdef, metatype}; 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 IS_UTF8_FN: &str = "__lf_is_utf8";
pub(crate) const DROP_BUFFER_FN: &str = "luaffi_drop_buffer"; 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 { unsafe extern "C" fn __is_utf8(ptr: *const u8, len: usize) -> bool {
debug_assert!(!ptr.is_null()); debug_assert!(!ptr.is_null());
simdutf8::basic::from_utf8(unsafe { slice::from_raw_parts(ptr, len) }).is_ok() 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) { unsafe extern "C" fn __drop_buffer(buf: *mut lua_buffer) {
debug_assert!(!buf.is_null()); debug_assert!(!buf.is_null());
debug_assert!(!unsafe { (*buf).__ptr.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 { pub(crate) fn null() -> Self {
Self { Self {
__ptr: ptr::null(), __ptr: ptr::null(),
@ -71,6 +72,7 @@ impl lua_buffer {
} }
} }
#[cfg(feature = "option_string_abi")]
pub(crate) fn null() -> Self { pub(crate) fn null() -> Self {
Self { Self {
__ptr: ptr::null_mut(), __ptr: ptr::null_mut(),
@ -228,7 +230,14 @@ impl_into_via!(&'static str, &'static BStr);
impl_into_via!(BString, Vec<u8>); impl_into_via!(BString, Vec<u8>);
impl_into_via!(String, BString); impl_into_via!(String, BString);
macro_rules! impl_optional_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) => { ($ty:ty) => {
unsafe impl<'s> FromFfi for Option<$ty> { unsafe impl<'s> FromFfi for Option<$ty> {
type From = <$ty as FromFfi>::From; type From = <$ty as FromFfi>::From;
@ -250,13 +259,13 @@ macro_rules! impl_optional_from {
} }
} }
}; };
} }
impl_optional_from!(&'s [u8]); impl_optional_from!(&'s [u8]);
impl_optional_from!(&'s BStr); impl_optional_from!(&'s BStr);
impl_optional_from!(&'s str); impl_optional_from!(&'s str);
macro_rules! impl_optional_into { macro_rules! impl_optional_into {
($ty:ty, $null:expr) => { ($ty:ty, $null:expr) => {
unsafe impl IntoFfi for Option<$ty> { unsafe impl IntoFfi for Option<$ty> {
type Into = <$ty as IntoFfi>::Into; type Into = <$ty as IntoFfi>::Into;
@ -273,11 +282,12 @@ macro_rules! impl_optional_into {
} }
} }
}; };
} }
impl_optional_into!(&'static [u8], lua_buf::null()); impl_optional_into!(&'static [u8], lua_buf::null());
impl_optional_into!(&'static BStr, lua_buf::null()); impl_optional_into!(&'static BStr, lua_buf::null());
impl_optional_into!(&'static str, lua_buf::null()); impl_optional_into!(&'static str, lua_buf::null());
impl_optional_into!(Vec<u8>, lua_buffer::null()); impl_optional_into!(Vec<u8>, lua_buffer::null());
impl_optional_into!(BString, lua_buffer::null()); impl_optional_into!(BString, lua_buffer::null());
impl_optional_into!(String, lua_buffer::null()); impl_optional_into!(String, lua_buffer::null());
}

View File

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

View File

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

View File

@ -1,23 +1,27 @@
use crate::utils::{ use crate::utils::{
StringLike, ffi_crate, is_optionlike, is_primitivelike, is_stringlike, is_unit, pat_ident, StringLike, ffi_crate, is_optionlike, is_primitivelike, is_resultlike, is_stringlike, is_unit,
syn_assert, syn_error, ty_name, pat_ident, syn_assert, syn_error, ty_name,
}; };
use darling::FromMeta;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote, quote_spanned}; use quote::{ToTokens, format_ident, quote, quote_spanned};
use std::{collections::HashSet, fmt, iter}; use std::{collections::HashSet, fmt, iter};
use syn::{ext::IdentExt, spanned::Spanned, *}; use syn::{ext::IdentExt, spanned::Spanned, *};
pub fn transform(mut imp: ItemImpl) -> Result<TokenStream> { #[derive(Debug, FromMeta)]
pub struct Args {}
pub fn transform(args: Args, mut imp: ItemImpl) -> Result<TokenStream> {
syn_assert!( syn_assert!(
imp.generics.params.is_empty(), imp.generics.params.is_empty(),
imp.generics, imp.generics,
"cannot be generic (not yet implemented)" "cannot be generic (not yet implemented)"
); );
let impls = generate_impls(&mut imp)?; let impls = generate_impls(&args, &mut imp)?;
let mod_name = format_ident!("__{}_metatype", ty_name(&imp.self_ty)?); let mod_name = format_ident!("__{}_metatype", ty_name(&imp.self_ty)?);
Ok(quote_spanned!(imp.self_ty.span() => Ok(quote!(
#imp #imp
#[doc(hidden)] #[doc(hidden)]
@ -46,7 +50,7 @@ impl Registry {
} }
} }
fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> { fn generate_impls(_args: &Args, imp: &mut ItemImpl) -> Result<TokenStream> {
let ffi = ffi_crate(); let ffi = ffi_crate();
let ty = imp.self_ty.clone(); let ty = imp.self_ty.clone();
let ty_name = ty_name(&ty)?; let ty_name = ty_name(&ty)?;
@ -54,7 +58,27 @@ fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
let mut mms = HashSet::new(); let mut mms = HashSet::new();
let mut lua_drop = None; let mut lua_drop = None;
for func in get_ffi_functions(imp)? { // process lua includes
imp.attrs.retain(|attr| {
if attr.path().is_ident("include") {
if let Ok(path) = attr.parse_args::<LitStr>() {
registry.build.push(quote_spanned!(path.span() =>
b.include_lua(::std::include_str!(#path));
));
return false;
} else if let Ok(chunk) = attr.parse_args::<Block>() {
registry.build.push(quote_spanned!(chunk.span() =>
b.include_lua(#ffi::__internal::luaify_chunk!(#chunk));
));
return false;
}
}
true
});
// process extern "Lua-C" ffi functions
for func in parse_ffi_functions(imp)? {
if let Some(mm) = func.attrs.metamethod { if let Some(mm) = func.attrs.metamethod {
syn_assert!( syn_assert!(
mms.insert(mm), mms.insert(mm),
@ -63,10 +87,11 @@ fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
); );
} }
add_ffi_function(&mut registry, &func)?; generate_ffi_function(&mut registry, &func)?;
} }
for func in get_lua_functions(imp)? { // process extern "Lua" lua functions
for func in parse_lua_functions(imp)? {
if let Some(mm) = func.attrs.metamethod { if let Some(mm) = func.attrs.metamethod {
syn_assert!( syn_assert!(
mms.insert(mm), mms.insert(mm),
@ -75,24 +100,26 @@ fn generate_impls(imp: &mut ItemImpl) -> Result<TokenStream> {
); );
} }
if let Some(Metamethod::Gc) = func.attrs.metamethod { if func.attrs.metamethod == Some(Metamethod::Gc) {
lua_drop = Some(func); lua_drop = Some(func);
} else { } else {
add_lua_function(&mut registry, &func)?; generate_lua_function(&mut registry, &func)?;
} }
} }
// if no #[new] metamethod is defined, inject fallback new
if !mms.contains(&Metamethod::New) { if !mms.contains(&Metamethod::New) {
inject_fallback_new(&mut registry)?; inject_fallback_new(&mut registry)?;
} }
// inject __gc/drop
inject_merged_drop(&mut registry, lua_drop.as_ref())?; inject_merged_drop(&mut registry, lua_drop.as_ref())?;
let shims = &registry.shims; let shims = &registry.shims;
let build = &registry.build; let build = &registry.build;
let exports = generate_ffi_exports(&registry)?; let exports = generate_ffi_exports(&registry)?;
Ok(quote_spanned!(ty.span() => Ok(quote!(
impl #ty { #(#shims)* } impl #ty { #(#shims)* }
unsafe impl #ffi::Metatype for #ty { unsafe impl #ffi::Metatype for #ty {
@ -128,6 +155,25 @@ enum Metamethod {
Ipairs, Ipairs,
} }
impl Metamethod {
fn narg(&self) -> Option<usize> {
Some(match *self {
Self::Eq
| Self::Lt
| Self::Le
| Self::Concat
| Self::Add
| Self::Sub
| Self::Mul
| Self::Div
| Self::Mod
| Self::Pow => 2,
Self::Gc | Self::Len | Self::Unm | Self::ToString | Self::Pairs | Self::Ipairs => 1,
Self::Call | Self::New => return None,
})
}
}
impl TryFrom<&Ident> for Metamethod { impl TryFrom<&Ident> for Metamethod {
type Error = Error; type Error = Error;
@ -192,10 +238,10 @@ impl ToTokens for Metamethod {
struct FfiFunction { struct FfiFunction {
name: Ident, name: Ident,
is_async: bool, params: Vec<(Ident, Type)>,
params: Vec<PatType>,
ret: Type, ret: Type,
attrs: FfiFunctionAttrs, attrs: FfiFunctionAttrs,
is_async: bool,
} }
#[derive(Default)] #[derive(Default)]
@ -203,8 +249,8 @@ struct FfiFunctionAttrs {
metamethod: Option<Metamethod>, metamethod: Option<Metamethod>,
} }
fn get_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> { fn parse_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
let mut funcs = vec![]; let mut parsed = vec![];
for item in imp.items.iter_mut() { for item in imp.items.iter_mut() {
if let ImplItem::Fn(func) = item if let ImplItem::Fn(func) = item
@ -224,30 +270,31 @@ fn get_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
.sig .sig
.inputs .inputs
.iter() .iter()
.map(|arg| match arg { .map(|arg| {
Ok(match arg {
FnArg::Receiver(recv) => { FnArg::Receiver(recv) => {
let ty = &recv.ty; (Ident::new("self", recv.span()), (*recv.ty).clone())
parse_quote_spanned!(ty.span() => self: #ty)
} }
FnArg::Typed(ty) => ty.clone(), FnArg::Typed(ty) => (pat_ident(&ty.pat)?.clone(), (*ty.ty).clone()),
}) })
.collect(); })
.collect::<Result<_>>()?;
let ret = match func.sig.output { let ret = match func.sig.output {
ReturnType::Default => parse_quote!(()), ReturnType::Default => parse_quote!(()),
ReturnType::Type(_, ref ty) => (**ty).clone(), ReturnType::Type(_, ref ty) => (**ty).clone(),
}; };
for param in params.iter() { for (name, ty) in params.iter() {
// double underscores are reserved for generated glue code // double underscores are reserved for glue code
syn_assert!( syn_assert!(
!pat_ident(&param.pat)?.to_string().starts_with("__"), !name.to_string().starts_with("__"),
param.pat, name,
"parameter names should not start with `__`" "parameter names should not start with `__`"
); );
// lifetime should be determined by the caller (lua) // lifetime should be determined by the caller (which is lua)
if let Type::Reference(ref ty) = *param.ty { if let Type::Reference(ty) = ty {
syn_assert!( syn_assert!(
ty.lifetime.is_none(), ty.lifetime.is_none(),
ty.lifetime, ty.lifetime,
@ -257,21 +304,30 @@ fn get_ffi_functions(imp: &mut ItemImpl) -> Result<Vec<FfiFunction>> {
} }
let attrs = parse_ffi_function_attrs(&mut func.attrs)?; let attrs = parse_ffi_function_attrs(&mut func.attrs)?;
attrs.metamethod.map(|mm| document_metamethod(func, mm));
document_ffi_function(func); if let Some(mm) = attrs.metamethod
&& let Some(narg) = mm.narg()
{
syn_assert!(
params.len() == narg,
func.sig.inputs,
"expected {narg} parameters for `{mm}` metamethod"
);
}
funcs.push(FfiFunction { parsed.push(FfiFunction {
name: func.sig.ident.clone(), name: func.sig.ident.clone(),
is_async: func.sig.asyncness.is_some(),
params, params,
ret, ret,
attrs, attrs,
is_async: func.sig.asyncness.is_some(),
}); });
document_ffi_function(func, parsed.last().unwrap());
} }
} }
Ok(funcs) Ok(parsed)
} }
fn parse_ffi_function_attrs(attrs: &mut Vec<Attribute>) -> Result<FfiFunctionAttrs> { fn parse_ffi_function_attrs(attrs: &mut Vec<Attribute>) -> Result<FfiFunctionAttrs> {
@ -279,14 +335,10 @@ fn parse_ffi_function_attrs(attrs: &mut Vec<Attribute>) -> Result<FfiFunctionAtt
let mut i = 0; let mut i = 0;
while let Some(attr) = attrs.get(i) { while let Some(attr) = attrs.get(i) {
if let Some(name) = attr.path().get_ident() if let Some(name) = attr.path().get_ident()
&& let Ok(method) = Metamethod::try_from(name) && let Ok(mm) = Metamethod::try_from(&name.unraw())
{ {
match method { syn_assert!(mm != Metamethod::Gc, attr, "implement `Drop` instead");
Metamethod::Gc => syn_error!(attr, "implement `Drop` instead"), parsed.metamethod = Some(mm);
_ => {}
}
parsed.metamethod = Some(method);
attrs.remove(i); attrs.remove(i);
} else { } else {
i += 1; i += 1;
@ -345,7 +397,7 @@ fn get_ffi_ret_type(ty: &Type) -> FfiReturnType {
} }
} }
fn add_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<()> { fn generate_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<()> {
let ffi = ffi_crate(); let ffi = ffi_crate();
let ty = &registry.ty; let ty = &registry.ty;
let func_name = &func.name; let func_name = &func.name;
@ -360,144 +412,153 @@ fn add_ffi_function(registry: &mut Registry, func: &FfiFunction) -> Result<()> {
let func_params = &func.params; // target function parameters let func_params = &func.params; // target function parameters
let func_ret = &func.ret; // target function return type let func_ret = &func.ret; // target function return type
let mut func_args = vec![]; // target function arguments let mut func_args = vec![]; // target function arguments
let mut shim_params = vec![]; // shim function parameters let mut shim_params = vec![]; // shim function parameters
let mut shim_ret = if func.is_async { let mut asserts = vec![]; // compile-time asserts
// shim function return type
quote_spanned!(func_ret.span() => #ffi::future::lua_future<impl ::std::future::Future<Output = #func_ret>>)
} else {
quote_spanned!(func_ret.span() => <#func_ret as #ffi::IntoFfi>::Into)
};
let mut asserts = vec![]; // compile-time builder asserts
let mut build = vec![]; // ffi builder body let mut build = vec![]; // ffi builder body
// for __new metamethods, ignore the first argument (ctype of self, for which there is no match func.attrs.metamethod {
// equivalent in C) Some(Metamethod::New) => {
if func.attrs.metamethod == Some(Metamethod::New) { // for __new metamethods, ignore the first argument (ctype of self, for which there is
// no equivalent in C)
build.push(quote!( build.push(quote!(
b.param_ignored(); b.param_ignored();
)); ));
} }
Some(Metamethod::Eq) => {
// for __eq metamethods, set special eq mode to return false on argument type mismatch
// instead of throwing
build.push(quote!(
b.set_eqmm(true);
));
}
_ => {}
}
for (i, param) in func_params.iter().enumerate() { for (i, (name, func_param)) in func_params.iter().enumerate() {
let func_param = &param.ty; let span = func_param.span();
let name = name.unraw().to_string();
let shim_param = format_ident!("arg{i}"); let shim_param = format_ident!("arg{i}");
let name = pat_ident(&param.pat)?.unraw().to_string();
match get_ffi_param_type(func_param) { match get_ffi_param_type(func_param) {
FfiParameterType::Default => { FfiParameterType::Default => {
shim_params.push(quote_spanned!(func_param.span() => shim_params.push(quote_spanned!(span =>
#shim_param: <#func_param as #ffi::FromFfi>::From #shim_param: <#func_param as #ffi::FromFfi>::From
)); ));
func_args.push(quote_spanned!(param.pat.span() => func_args.push(quote_spanned!(span =>
<#func_param as #ffi::FromFfi>::convert(#shim_param) <#func_param as #ffi::FromFfi>::convert(#shim_param)
)); ));
build.push(quote_spanned!(param.pat.span() => build.push(if func.attrs.metamethod == Some(Metamethod::Eq) {
quote_spanned!(span =>
// preliminary `return false` for type mismatch in __eq mode. we just check
// against the ctype of Self because that's what __eq should be comparing
// anyway.
b.param_ctchecked::<#func_param, Self>(#name);
)
} else {
quote_spanned!(span =>
b.param::<#func_param>(#name); b.param::<#func_param>(#name);
)); )
});
} }
ty @ (FfiParameterType::StringLike(str) | FfiParameterType::OptionStringLike(str)) => { ty @ (FfiParameterType::StringLike(str) | FfiParameterType::OptionStringLike(str)) => {
let shim_param_len = format_ident!("arg{i}_len"); let shim_param_len = format_ident!("arg{i}_len");
shim_params.push(quote_spanned!(func_param.span() => shim_params.push(quote_spanned!(span =>
#shim_param: ::std::option::Option<&::std::primitive::u8>, #shim_param: ::std::option::Option<&::std::primitive::u8>,
#shim_param_len: ::std::primitive::usize #shim_param_len: ::std::primitive::usize
)); ));
let allow_nil = matches!(ty, FfiParameterType::OptionStringLike(_)); let allow_nil = matches!(ty, FfiParameterType::OptionStringLike(_));
let check_utf8 = matches!(str, StringLike::Str); let check_utf8 = matches!(str, StringLike::Str);
let mut func_arg = quote_spanned!(func_param.span() => let mut func_arg = quote_spanned!(span =>
#shim_param.map(|s| ::std::slice::from_raw_parts(s, #shim_param_len)) #shim_param.map(|s| ::std::slice::from_raw_parts(s, #shim_param_len))
); );
func_arg = match str { func_arg = match str {
StringLike::SliceU8 => func_arg, StringLike::SliceU8 => func_arg,
StringLike::BStr => { StringLike::BStr => {
quote_spanned!(func_param.span() => #func_arg.map(::bstr::BStr::new)) quote_spanned!(span => #func_arg.map(::bstr::BStr::new))
} }
StringLike::Str => { StringLike::Str => {
quote_spanned!(func_param.span() => #func_arg.map(|s| { quote_spanned!(span => #func_arg.map(|s| {
::std::debug_assert!(::std::str::from_utf8(s).is_ok()); ::std::debug_assert!(::std::str::from_utf8(s).is_ok());
::std::str::from_utf8_unchecked(s) ::std::str::from_utf8_unchecked(s)
})) }))
} }
}; };
if !allow_nil { if !allow_nil {
func_arg = quote_spanned!(func_param.span() => { func_arg = quote_spanned!(span => {
let arg = #func_arg; let arg = #func_arg;
::std::debug_assert!(arg.is_some()); ::std::debug_assert!(arg.is_some());
arg.unwrap_unchecked() arg.unwrap_unchecked()
}); });
} }
func_args.push(func_arg); func_args.push(func_arg);
build.push(quote_spanned!(param.pat.span() => build.push(quote_spanned!(span =>
b.param_str(#name, #allow_nil, #check_utf8); b.param_str(#name, #allow_nil, #check_utf8);
)); ));
} }
} }
} }
// shim function body let func_call = quote!(Self::#func_name(#(#func_args),*)); // target function call
let mut shim_body = if func.is_async {
// for async functions, wrapped the returned future in lua_future
quote_spanned!(func_name.span() => #ffi::future::lua_future::new(Self::#func_name(#(#func_args),*)))
} else {
quote_spanned!(func_name.span() => <#func_ret as #ffi::IntoFfi>::convert(Self::#func_name(#(#func_args),*)))
};
if !func.is_async { // shim function body and return type
let (shim_body, shim_ret) = if func.is_async {
let span = func_ret.span();
(
// for async functions, wrapped the returned future in lua_future
quote_spanned!(span => #ffi::future::lua_future::new(#func_call)),
quote_spanned!(span => #ffi::future::lua_future<impl ::std::future::Future<Output = #func_ret>>),
)
} else {
let span = func_ret.span();
match get_ffi_ret_type(&func_ret) { match get_ffi_ret_type(&func_ret) {
FfiReturnType::Void => { FfiReturnType::Void => {
asserts.push(quote_spanned!(func_ret.span() => asserts.push(quote_spanned!(span =>
<<#func_ret as #ffi::IntoFfi>::Into as #ffi::Type>::ty() == #ffi::TypeType::Void <<#func_ret as #ffi::IntoFfi>::Into as #ffi::Type>::ty() == #ffi::TypeType::Void
)); ));
(func_call, quote_spanned!(span => ()))
} }
FfiReturnType::ByValue => { FfiReturnType::ByValue => {
asserts.push(quote_spanned!(func_ret.span() => asserts.push(quote_spanned!(span =>
<#func_ret as #ffi::IntoFfi>::convention() == #ffi::FfiReturnConvention::ByValue <#func_ret as #ffi::IntoFfi>::convention() == #ffi::FfiReturnConvention::ByValue
)); ));
(
quote_spanned!(span => <#func_ret as #ffi::IntoFfi>::convert(#func_call)),
quote_spanned!(span => <#func_ret as #ffi::IntoFfi>::Into),
)
} }
FfiReturnType::ByOutParam => { FfiReturnType::ByOutParam => {
asserts.push(quote_spanned!(func_ret.span() => asserts.push(quote_spanned!(span =>
<#func_ret as #ffi::IntoFfi>::convention() == #ffi::FfiReturnConvention::ByOutParam <#func_ret as #ffi::IntoFfi>::convention() == #ffi::FfiReturnConvention::ByOutParam
)); ));
let out = quote_spanned!(span => out: *mut <#func_ret as #ffi::IntoFfi>::Into);
shim_params.insert(0, quote_spanned!(func_ret.span() => out: *mut #shim_ret)); shim_params.insert(0, out);
(
(shim_body, shim_ret) = ( quote_spanned!(span => ::std::ptr::write(out, <#func_ret as #ffi::IntoFfi>::convert(#func_call))),
quote_spanned!(func_ret.span() => ::std::ptr::write(out, #shim_body)), quote_spanned!(span => ()),
quote_spanned!(func_ret.span() => ()), )
); }
} }
}; };
}
// build.push(quote_spanned!(func_name.span() => build.push(if func.is_async {
// b.call_inferred(#c_name, Self::#func_name); // we can't name the future from an async function, so instead we provide a closure with a
// )); // "dummy call" that never gets called so that the builder can infer the return type from
// the closure
build.push({
let infer_args = iter::repeat_n(quote!(::std::unreachable!()), func_params.len()); let infer_args = iter::repeat_n(quote!(::std::unreachable!()), func_params.len());
let infer = if func.is_async { quote!(b.call_inferred(#c_name, || {
quote!(|| #ffi::future::lua_future::new(Self::#func_name(#(#infer_args),*))) #ffi::future::lua_future::new(Self::#func_name(#(#infer_args),*))
});)
} else { } else {
quote!(|| Self::#func_name(#(#infer_args),*)) quote!(b.call::<#func_ret>(#c_name);)
};
quote_spanned!(func_name.span() => b.call_inferred(#c_name, #infer);)
}); });
registry.build.push(quote_spanned!(func_name.span() => registry.build.push(quote!(#(::std::assert!(#asserts);)*));
#(::std::assert!(#asserts);)*
));
registry.build.push(match func.attrs.metamethod { registry.build.push(match func.attrs.metamethod {
Some(ref mm) => quote_spanned!(func_name.span() => Some(mm) => quote!(b.metatable(#mm, |b| { #(#build)* });),
b.metatable(#mm, |b| { #(#build)* }); None => quote!(b.index(#lua_name, |b| { #(#build)* });),
),
None => quote_spanned!(func_name.span() =>
b.index(#lua_name, |b| { #(#build)* });
),
}); });
registry.shims.push(parse_quote_spanned!(func_name.span() => registry.shims.push(parse_quote!(
#[unsafe(export_name = #c_name)] #[unsafe(export_name = #c_name)]
unsafe extern "C" fn #shim_name(#(#shim_params),*) -> #shim_ret { unsafe { #shim_body } } unsafe extern "C" fn #shim_name(#(#shim_params),*) -> #shim_ret { unsafe { #shim_body } }
)); ));
@ -509,7 +570,7 @@ fn generate_ffi_exports(registry: &Registry) -> Result<TokenStream> {
let ty = &registry.ty; let ty = &registry.ty;
let names = registry.shims.iter().map(|f| &f.sig.ident); let names = registry.shims.iter().map(|f| &f.sig.ident);
Ok(quote_spanned!(ty.span() => Ok(quote!(
// this ensures ffi function symbol exports are actually present in the resulting binary, // this ensures ffi function symbol exports are actually present in the resulting binary,
// otherwise they may get dead code-eliminated before it reaches the linker // otherwise they may get dead code-eliminated before it reaches the linker
#[used] #[used]
@ -531,8 +592,8 @@ struct LuaFunctionAttrs {
metamethod: Option<Metamethod>, metamethod: Option<Metamethod>,
} }
fn get_lua_functions(imp: &mut ItemImpl) -> Result<Vec<LuaFunction>> { fn parse_lua_functions(imp: &mut ItemImpl) -> Result<Vec<LuaFunction>> {
let mut funcs = vec![]; let mut parsed = vec![];
for item in imp.items.iter_mut() { for item in imp.items.iter_mut() {
if let ImplItem::Fn(func) = item if let ImplItem::Fn(func) = item
@ -546,41 +607,70 @@ fn get_lua_functions(imp: &mut ItemImpl) -> Result<Vec<LuaFunction>> {
"cannot be generic" "cannot be generic"
); );
syn_assert!(
func.sig.constness.is_none(),
func.sig.constness,
"cannot be const"
);
let mut params: Vec<_> = func let mut params: Vec<_> = func
.sig .sig
.inputs .inputs
.iter() .iter()
.map(|arg| { .map(|arg| match arg {
Ok(match arg { FnArg::Receiver(recv) => parse_quote_spanned!(recv.span() => self),
FnArg::Receiver(recv) => Pat::Type(parse_quote_spanned!(recv.span() => FnArg::Typed(ty) => (*ty.pat).clone(), // ignore parameter type (only used for documentation purposes)
self: cdata
)),
FnArg::Typed(ty) => Pat::Type(ty.clone()),
}) })
}) .collect();
.collect::<Result<_>>()?;
if let Some(ref variadic) = func.sig.variadic { if let Some(ref variadic) = func.sig.variadic {
// need to transform variadic to the `varidic!()` luaify builtin macro because we
// luaify a closure below, and closures don't have variadics.
params.push(parse_quote_spanned!(variadic.span() => variadic!())); params.push(parse_quote_spanned!(variadic.span() => variadic!()));
} }
let attrs = parse_lua_function_attrs(&mut func.attrs)?; let attrs = parse_lua_function_attrs(&mut func.attrs)?;
attrs.metamethod.map(|mm| document_metamethod(func, mm));
document_lua_function(func); if let Some(mm) = attrs.metamethod
&& let Some(narg) = mm.narg()
{
syn_assert!(
params.len() == narg,
func.sig.inputs,
"expected {narg} parameters for `{mm}` metamethod"
);
}
funcs.push(LuaFunction { parsed.push(LuaFunction {
name: func.sig.ident.clone(), name: func.sig.ident.clone(),
params, params,
body: func.block.clone(), body: func.block.clone(),
attrs, attrs,
}); });
document_lua_function(func, parsed.last().unwrap());
stub_lua_function(func)?; stub_lua_function(func)?;
} }
} }
Ok(funcs) Ok(parsed)
}
fn parse_lua_function_attrs(attrs: &mut Vec<Attribute>) -> Result<LuaFunctionAttrs> {
let mut parsed = LuaFunctionAttrs::default();
let mut i = 0;
while let Some(attr) = attrs.get(i) {
if let Some(name) = attr.path().get_ident()
&& let Ok(mm) = Metamethod::try_from(&name.unraw())
{
parsed.metamethod = Some(mm);
attrs.remove(i);
} else {
i += 1;
}
}
Ok(parsed)
} }
fn stub_lua_function(func: &mut ImplItemFn) -> Result<()> { fn stub_lua_function(func: &mut ImplItemFn) -> Result<()> {
@ -592,84 +682,46 @@ fn stub_lua_function(func: &mut ImplItemFn) -> Result<()> {
func.attrs.push(parse_quote!(#[allow(unused)])); func.attrs.push(parse_quote!(#[allow(unused)]));
func.block.stmts.clear(); func.block.stmts.clear();
func.block.stmts.push(parse_quote!( func.block.stmts.push(parse_quote!(
::std::unreachable!("cannot call lua function from rust"); const fn has_annotation<T: #ffi::Annotate>() {}
)); ));
let inputs = &mut func.sig.inputs; // convert `...` variadic to a `rest: luaffi::marker::Many` parameter
let output = &mut func.sig.output;
for input in inputs.iter_mut() {
if let FnArg::Typed(pat) = input
&& let Ok(stub) = stub_lua_type(&pat.ty)
{
pat.ty = stub.into();
}
}
if let ReturnType::Type(_, ty) = output
&& let Ok(stub) = stub_lua_type(ty)
{
*ty = stub.into();
}
if let Some(ref variadic) = func.sig.variadic { if let Some(ref variadic) = func.sig.variadic {
let ty = quote_spanned!(variadic.span() => variadic); let param = Ident::new("rest", variadic.span());
inputs.push(parse_quote!(rest: #ffi::__internal::stub_types::#ty)); let ty = quote_spanned!(variadic.span() => many);
func.sig.variadic = None; func.sig.variadic = None;
func.sig
.inputs
.push(parse_quote!(#param: #ffi::marker::#ty));
} }
for param in func.sig.inputs.iter() {
let ty = match param {
FnArg::Receiver(recv) => &recv.ty,
FnArg::Typed(ty) => &ty.ty,
};
// temporary assertion until we implement annotation generation
func.block.stmts.push(parse_quote!(
has_annotation::<#ty>();
));
}
// temporary assertion until we implement annotation generation
if let ReturnType::Type(_, ref ty) = func.sig.output {
func.block.stmts.push(parse_quote!(
has_annotation::<#ty>();
));
}
func.block.stmts.push(parse_quote!(
::std::unreachable!(r#"cannot call extern "Lua" function from rust"#);
));
Ok(()) Ok(())
} }
fn stub_lua_type(ty: &Type) -> Result<Type> { fn generate_lua_function(registry: &mut Registry, func: &LuaFunction) -> Result<()> {
let ffi = ffi_crate();
let span = ty.span();
let ty = if let Type::Infer(_) = ty {
quote_spanned!(span => any)
} else {
match ty_name(ty)?.to_string().as_str() {
"any" => quote_spanned!(span => any),
"nil" => quote_spanned!(span => nil),
"boolean" => quote_spanned!(span => boolean),
"lightuserdata" => quote_spanned!(span => lightuserdata),
"number" => quote_spanned!(span => number),
"integer" => quote_spanned!(span => integer),
"string" => quote_spanned!(span => string),
"table" => quote_spanned!(span => table),
"function" => quote_spanned!(span => function),
"userdata" => quote_spanned!(span => userdata),
"thread" => quote_spanned!(span => thread),
"cdata" => quote_spanned!(span => cdata),
_ => syn_error!(ty, "unknown lua type"),
}
};
Ok(parse_quote!(#ffi::__internal::stub_types::#ty))
}
fn parse_lua_function_attrs(attrs: &mut Vec<Attribute>) -> Result<LuaFunctionAttrs> {
let mut parsed = LuaFunctionAttrs::default();
let mut i = 0;
while let Some(attr) = attrs.get(i) {
if let Some(name) = attr.path().get_ident()
&& let Ok(method) = Metamethod::try_from(name)
{
match method {
Metamethod::New => syn_error!(attr, r#"cannot be applied to a lua function"#),
_ => {}
}
parsed.metamethod = Some(method);
attrs.remove(i);
} else {
i += 1;
}
}
Ok(parsed)
}
fn add_lua_function(registry: &mut Registry, func: &LuaFunction) -> Result<()> {
let ffi = ffi_crate(); let ffi = ffi_crate();
let luaify = quote!(#ffi::__internal::luaify!); let luaify = quote!(#ffi::__internal::luaify!);
let func_name = &func.name; let func_name = &func.name;
@ -678,12 +730,8 @@ fn add_lua_function(registry: &mut Registry, func: &LuaFunction) -> Result<()> {
let name = func_name.unraw().to_string(); let name = func_name.unraw().to_string();
registry.build.push(match func.attrs.metamethod { registry.build.push(match func.attrs.metamethod {
Some(ref mm) => quote_spanned!(func_name.span() => Some(mm) => quote!(b.metatable_raw(#mm, #luaify(|#(#params),*| #body));),
b.metatable_raw(#mm, #luaify(|#(#params),*| #body)); None => quote!(b.index_raw(#name, #luaify(|#(#params),*| #body));),
),
None => quote_spanned!(func_name.span() =>
b.index_raw(#name, #luaify(|#(#params),*| #body));
),
}); });
Ok(()) Ok(())
@ -708,50 +756,40 @@ fn inject_merged_drop(registry: &mut Registry, lua: Option<&LuaFunction>) -> Res
let luaify = quote!(#ffi::__internal::luaify!); let luaify = quote!(#ffi::__internal::luaify!);
let ty = &registry.ty; let ty = &registry.ty;
let shim_name = format_ident!("__ffi_drop"); let shim_name = format_ident!("__ffi_drop");
let c_name = format_ident!("{}_drop", ty.unraw()); let c_name = format_ident!("__{}_drop", ty.unraw());
let c_name_str = c_name.to_string(); let c_name_str = c_name.to_string();
if let Some(lua) = lua { if let Some(lua) = lua {
syn_assert!( let param = pat_ident(&lua.params[0])?;
lua.params.len() == 1,
lua.name,
"finaliser must take exactly one parameter"
);
syn_assert!(
pat_ident(&lua.params[0])? == "self",
lua.params[0],
"finaliser parameter must be `self`"
);
let params = &lua.params;
let body = &lua.body; let body = &lua.body;
registry.build.push(quote_spanned!(ty.span() => syn_assert!(param == "self", param, "finaliser parameter must be `self`");
registry.build.push(quote!(
if ::std::mem::needs_drop::<Self>() { if ::std::mem::needs_drop::<Self>() {
// if we have both a lua-side finaliser and a rust drop, then merge the finalisers // if we have both a lua-side finaliser and a rust drop, then merge the finalisers
// by doing the lua part first then drop rust // by doing the lua part first then drop rust
b.declare::<#ffi::UnsafeExternCFn<(*mut Self,), ()>>(#c_name_str); b.declare::<#ffi::ExternCFn<(*mut Self,), ()>>(#c_name_str);
b.metatable_raw("gc", #luaify(|self| { b.metatable_raw("gc", #luaify(|self| {
raw!(#luaify(#body)); // embed the lua part inside a do block raw!(#luaify(#body)); // embed the lua part inside a do block
__C::#c_name(self); __C::#c_name(self);
})); }));
} else { } else {
// we only have a lua-side finaliser // we only have a lua-side finaliser
b.metatable_raw("gc", #luaify(|#(#params),*| #body)); b.metatable_raw("gc", #luaify(|self| #body));
} }
)); ));
} else { } else {
registry.build.push(quote_spanned!(ty.span() => registry.build.push(quote!(
if ::std::mem::needs_drop::<Self>() { if ::std::mem::needs_drop::<Self>() {
// we only have a rust drop // we only have a rust drop
b.declare::<#ffi::UnsafeExternCFn<(*mut Self,), ()>>(#c_name_str); b.declare::<#ffi::ExternCFn<(*mut Self,), ()>>(#c_name_str);
b.metatable_raw("gc", ::std::format_args!("__C.{}", #c_name_str)); b.metatable_raw("gc", ::std::format_args!("__C.{}", #c_name_str));
} }
)); ));
} }
registry.shims.push(parse_quote_spanned!(ty.span() => registry.shims.push(parse_quote!(
#[unsafe(export_name = #c_name_str)] #[unsafe(export_name = #c_name_str)]
unsafe extern "C" fn #shim_name(ptr: *mut Self) { unsafe extern "C" fn #shim_name(ptr: *mut Self) {
unsafe { ::std::ptr::drop_in_place(ptr) } unsafe { ::std::ptr::drop_in_place(ptr) }
@ -761,44 +799,176 @@ fn inject_merged_drop(registry: &mut Registry, lua: Option<&LuaFunction>) -> Res
Ok(()) Ok(())
} }
fn document_ffi_function(func: &mut ImplItemFn) { // the transparency makes it work in dark mode too
func.attrs.insert(0, parse_quote!(#[doc = // const FFI_COLOR: &str = "rgba(255, 222, 118, 0.3)"; // yellow
r#"<span class="stab" title="This is a C/FFI function." style="float: right; font-weight: 500; margin-left: 3px; padding-left: 5px; padding-right: 5px;">FFI</span>"# // const LUA_COLOR: &str = "rgba(188, 222, 255, 0.3)"; // blue
])); const FALLIBLE_COLOR: &str = "rgba(255, 168, 168, 0.3)"; // red
const ASYNC_COLOR: &str = "rgba(188, 222, 255, 0.3)"; // blue
const METAMETHOD_COLOR: &str = "rgba(255, 222, 118, 0.3)"; // yellow
struct StabConfig {
text: &'static str,
desc: &'static str,
color: &'static str,
strong: bool,
} }
fn document_lua_function(func: &mut ImplItemFn) { fn generate_stab(
func.attrs.insert(0, parse_quote!(#[doc = StabConfig {
r#"<span class="stab" title="This is a Lua function." style="float: right; font-weight: 500; margin-left: 3px; padding-left: 5px; padding-right: 5px;">Lua</span>"# text,
])); desc,
color,
strong,
}: StabConfig,
) -> Attribute {
let attrs = vec![
("class", "stab".into()),
("title", desc.into()),
("aria-label", desc.into()),
("style", {
let mut style = vec![
("float", "right"),
("margin", "1px"),
("margin-left", "4px"),
("padding-left", "5px"),
("padding-right", "5px"),
("background", color),
];
if strong {
style.push(("font-weight", "600"));
}
style
.into_iter()
.map(|(k, v)| format!("{k}: {v}"))
.collect::<Vec<_>>()
.join("; ")
}),
];
let attrs = attrs
.into_iter()
.map(|(k, v)| format!(r#"{k}="{v}""#))
.collect::<Vec<_>>()
.join(" ");
let tag = format!(r#"<span {attrs}>{text}</span>"#);
parse_quote!(#[doc = #tag])
} }
fn document_metamethod(func: &mut ImplItemFn, method: Metamethod) { fn document_ffi_function(item: &mut ImplItemFn, func: &FfiFunction) {
let s = match method { let mut attrs = vec![];
Metamethod::Eq => "This is a metamethod which is called by the `==` operator.".into(),
Metamethod::Len => "This is a metamethod which is called by the `#` operator.".into(), // attrs.push(generate_stab(StabConfig {
Metamethod::Lt => "This is a metamethod which is called by the `<` operator.".into(), // text: "FFI",
Metamethod::Le => "This is a metamethod which is called by the `<=` operator.".into(), // desc: "This function is implemented in Rust and called via FFI.",
Metamethod::Concat => "This is a metamethod which is called by the `..` operator.".into(), // color: FFI_COLOR,
Metamethod::Add => "This is a metamethod which is called by the `+` operator.".into(), // strong: true,
Metamethod::Sub => "This is a metamethod which is called by the `-` operator.".into(), // }));
Metamethod::Mul => "This is a metamethod which is called by the `*` operator.".into(),
Metamethod::Div => "This is a metamethod which is called by the `/` operator.".into(), generate_stab_fallible(item).map(|stab| attrs.push(stab));
Metamethod::Mod => "This is a metamethod which is called by the `%` operator.".into(), generate_stab_async(item).map(|stab| attrs.push(stab));
Metamethod::Pow => "This is a metamethod which is called by the `^` operator.".into(), generate_stab_metamethod(func.attrs.metamethod).map(|(stab, help)| {
Metamethod::Unm => "This is a metamethod which is called by the `-` operator.".into(), attrs.push(stab);
item.attrs.push(help);
});
item.attrs.splice(0..0, attrs);
}
fn document_lua_function(item: &mut ImplItemFn, func: &LuaFunction) {
let mut attrs = vec![];
// attrs.push(generate_stab(StabConfig {
// text: "Lua",
// desc: "This function is implemented in Lua.",
// color: LUA_COLOR,
// strong: true,
// }));
generate_stab_fallible(item).map(|stab| attrs.push(stab));
generate_stab_async(item).map(|stab| attrs.push(stab));
generate_stab_metamethod(func.attrs.metamethod).map(|(stab, help)| {
attrs.push(stab);
item.attrs.push(help);
});
item.attrs.splice(0..0, attrs);
}
fn generate_stab_async(item: &ImplItemFn) -> Option<Attribute> {
item.sig.asyncness.as_ref().map(|_| {
generate_stab(StabConfig {
text: "Async",
desc: "This function is asynchronous and will yield the calling thread.",
color: ASYNC_COLOR,
strong: false,
})
})
}
fn generate_stab_fallible(item: &ImplItemFn) -> Option<Attribute> {
match item.sig.output {
ReturnType::Default => None,
ReturnType::Type(_, ref ty) => is_resultlike(ty).map(|_| {
generate_stab(StabConfig {
text: "Fallible",
desc: "This function is fallible and may throw an error on failure.",
color: FALLIBLE_COLOR,
strong: false,
})
}),
}
}
fn generate_stab_metamethod(mm: Option<Metamethod>) -> Option<(Attribute, Attribute)> {
mm.map(|mm| {
let stab = generate_stab(StabConfig {
text: "Metamethod",
desc: "This function is a metamethod.",
color: METAMETHOD_COLOR,
strong: false,
});
let help = match mm {
Metamethod::Eq => "This function is a metamethod which is called by the `==` operator.",
Metamethod::Len => "This function is a metamethod which is called by the `#` operator.",
Metamethod::Lt => "This function is a metamethod which is called by the `<` operator.",
Metamethod::Le => "This function is a metamethod which is called by the `<=` operator.",
Metamethod::Concat => {
"This function is a metamethod which is called by the `..` operator."
}
Metamethod::Call => {
"This function is a metamethod which can be called by calling \
`(...)` on the value directly."
}
Metamethod::Add => "This function is a metamethod which is called by the `+` operator.",
Metamethod::Sub => "This function is a metamethod which is called by the `-` operator.",
Metamethod::Mul => "This function is a metamethod which is called by the `*` operator.",
Metamethod::Div => "This function is a metamethod which is called by the `/` operator.",
Metamethod::Mod => "This function is a metamethod which is called by the `%` operator.",
Metamethod::Pow => "This function is a metamethod which is called by the `^` operator.",
Metamethod::Unm => "This function is a metamethod which is called by the `-` operator.",
Metamethod::ToString => { Metamethod::ToString => {
"This is a metamethod which can be called by the `tostring` built-in function.".into() "This function is a metamethod which is called by the \
[`tostring(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-tostring) \
built-in function."
} }
Metamethod::Pairs => { Metamethod::Pairs => {
"This is a metamethod which can be called by the `pairs` built-in function.".into() "This function is a metamethod which is called by the \
[`pairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-pairs) \
built-in function."
} }
Metamethod::Ipairs => { Metamethod::Ipairs => {
"This is a metamethod which can be called by the `ipairs` built-in function.".into() "This function is a metamethod which is called by the \
[`ipairs(...)`](https://www.lua.org/manual/5.1/manual.html#pdf-ipairs) \
built-in function."
} }
_ => format!("This is a metamethod and cannot be called directly."), _ => "This function is a metamethod and cannot be called directly.",
}; };
func.attrs.push(parse_quote!(#[doc = ""])); let help = format!("\n# Metamethod\n\n{help}");
func.attrs.push(parse_quote!(#[doc = #s])); (stab, parse_quote!(#[doc = #help]))
})
} }

View File

@ -1,5 +1,5 @@
use std::env; use std::env;
use syn::{spanned::Spanned, *}; use syn::{ext::IdentExt, spanned::Spanned, *};
macro_rules! syn_error { macro_rules! syn_error {
($src:expr, $($fmt:expr),+) => {{ ($src:expr, $($fmt:expr),+) => {{
@ -63,7 +63,7 @@ pub fn is_primitivelike(ty: &Type) -> bool {
Type::Path(path) => { Type::Path(path) => {
if let Some(name) = path.path.get_ident() { if let Some(name) = path.path.get_ident() {
return matches!( return matches!(
name.to_string().as_str(), name.unraw().to_string().as_str(),
"bool" "bool"
| "u8" | "u8"
| "u16" | "u16"
@ -115,31 +115,37 @@ pub fn is_stringlike(ty: &Type) -> Option<StringLike> {
&& ty.mutability.is_none() && ty.mutability.is_none()
&& ty.lifetime.is_none() && ty.lifetime.is_none()
{ {
match *ty.elem { Some(match *ty.elem {
Type::Slice(ref slice) => { Type::Slice(ref slice) => {
// match &[u8] // match &[u8]
if let Type::Path(ref path) = *slice.elem if let Type::Path(ref path) = *slice.elem
&& let Some(name) = path.path.get_ident() && let Some(name) = path.path.get_ident()
&& name == "u8"
{ {
return Some(StringLike::SliceU8); match name.unraw().to_string().as_str() {
"u8" => StringLike::SliceU8,
_ => return None,
}
} else {
return None;
} }
} }
Type::Path(ref path) => { Type::Path(ref path) => {
// match &str or &BStr // match &str or &BStr
if let Some(name) = path.path.get_ident() { if let Some(name) = path.path.get_ident() {
match name.to_string().as_str() { match name.unraw().to_string().as_str() {
"str" => return Some(StringLike::Str), "str" => StringLike::Str,
"BStr" => return Some(StringLike::BStr), "BStr" => StringLike::BStr,
_ => {} _ => return None,
}
} else {
return None;
} }
} }
} _ => return None,
_ => {} })
} } else {
}
None None
}
} }
pub fn is_optionlike(ty: &Type) -> Option<&Type> { pub fn is_optionlike(ty: &Type) -> Option<&Type> {
@ -157,3 +163,19 @@ pub fn is_optionlike(ty: &Type) -> Option<&Type> {
None 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

@ -14,3 +14,6 @@ proc-macro = true
proc-macro2 = "1.0.95" proc-macro2 = "1.0.95"
quote = "1.0.40" quote = "1.0.40"
syn = { version = "2.0.103", features = ["full", "visit-mut"] } syn = { version = "2.0.103", features = ["full", "visit-mut"] }
[dev-dependencies]
pretty_assertions = "1.4.1"

View File

@ -14,6 +14,12 @@ pub fn generate(expr: &Expr) -> Result<TokenStream> {
Ok(f.done()) 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)] #[derive(Default)]
struct Formatter { struct Formatter {
buf: String, buf: String,
@ -910,7 +916,7 @@ fn generate_pat_typed(f: &mut Formatter, typed: &PatType, cx: PatContext) -> Res
assert_no_attrs!(typed); assert_no_attrs!(typed);
match *typed.ty { match *typed.ty {
Type::Infer(_) => generate_pat(f, &typed.pat, cx), 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,57 @@
use crate::{generate::generate, transform::transform}; //! # luaify
//!
//! A macro for generating Lua code from Rust syntax.
//!
//! This macro performs a direct one-to-one translation of Rust expressions and blocks into
//! equivalent Lua code. For example,
//!
//! ```rust
//! use luaify::luaify;
//!
//! luaify!(|a, b| {
//! let c = a + b;
//!
//! fn inner_function(c: _) {
//! print(concat!("the sum of ", a, " and ", b, " is ", c));
//! c
//! }
//!
//! inner_function(c);
//! });
//! ```
//!
//! will translate to the following equivalent Lua code (embedded as an [`&str`] or [`String`] in
//! Rust):
//!
//! ```lua
//! function(a, b)
//! local c = a + b
//!
//! local function inner_function(c)
//! print("the sum of " .. a .. " and " .. b .. " is " .. c)
//! return c
//! end
//!
//! inner_function(c)
//! end
//! ```
//!
//! This macro accepts a smaller subset of all valid Rust syntax due to the difference between
//! Rust's expression-based syntax and Lua's statement-based syntax. Most Rust syntax are translated
//! to their equivalents in Lua, however the following features are not supported and will result in
//! a compile-time error:
//!
//! - static typing (typed local variables, parameters and return types in function signatures,
//! etc.)
//! - pattern matching (e.g. `match`, `if let`, `while let`)
//! - items other than statements or functions (e.g. `struct`, `enum`, `trait`, `impl`)
//! - block statements in expression position (e.g. `if`, `while`, `for`, `loop` that evaluate to a
//! value)
//! - expressions in statement position (except for function calls and assignments)
use crate::{
generate::{generate, generate_chunk},
transform::{transform, transform_chunk},
};
use proc_macro::TokenStream as TokenStream1; use proc_macro::TokenStream as TokenStream1;
use quote::ToTokens; use quote::ToTokens;
use syn::parse_macro_input; use syn::parse_macro_input;
@ -7,6 +60,7 @@ mod generate;
mod transform; mod transform;
mod utils; mod utils;
/// Generates Lua code for the given expression.
#[proc_macro] #[proc_macro]
pub fn luaify(input: TokenStream1) -> TokenStream1 { pub fn luaify(input: TokenStream1) -> TokenStream1 {
let mut expr = parse_macro_input!(input); let mut expr = parse_macro_input!(input);
@ -16,3 +70,14 @@ pub fn luaify(input: TokenStream1) -> TokenStream1 {
} }
.into() .into()
} }
/// Generates Lua code for the given block.
#[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 crate::utils::syn_error;
use quote::format_ident;
use std::mem;
use syn::{spanned::*, visit_mut::*, *}; use syn::{spanned::*, visit_mut::*, *};
pub fn transform(expr: &mut Expr) -> Result<()> { pub fn transform(expr: &mut Expr) -> Result<()> {
@ -9,6 +7,12 @@ pub fn transform(expr: &mut Expr) -> Result<()> {
visitor.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)] #[derive(Debug)]
struct Visitor { struct Visitor {
result: Result<()>, result: Result<()>,
@ -21,27 +25,6 @@ impl Visitor {
} }
impl VisitMut for 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) { fn visit_expr_unary_mut(&mut self, un: &mut ExprUnary) {
match self.transform_unary(un) { match self.transform_unary(un) {
res @ Err(_) => self.result = res, res @ Err(_) => self.result = res,
@ -58,147 +41,9 @@ impl VisitMut for Visitor {
} }
impl 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<()> { 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. // `--` indicates a comment in lua.
// //
// before: // before:
@ -210,7 +55,7 @@ impl Visitor {
&& let Expr::Unary(ref inner) = *un.expr && let Expr::Unary(ref inner) = *un.expr
&& let UnOp::Neg(_) = inner.op && let UnOp::Neg(_) = inner.op
{ {
un.expr = Box::new(parse_quote!((#inner))); un.expr = Box::new(parse_quote_spanned!(inner.span() => (#inner)));
} }
Ok(()) Ok(())

View File

@ -1,5 +1,4 @@
use std::fmt; use syn::*;
use syn::{ext::*, spanned::*, *};
macro_rules! syn_error { macro_rules! syn_error {
($src:expr, $($fmt:expr),+) => {{ ($src:expr, $($fmt:expr),+) => {{
@ -24,90 +23,3 @@ pub fn wrap_expr_block(expr: &Expr) -> Block {
expr => parse_quote!({ #expr }), 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] #[test]
fn raw_ident() { fn raw_ident() {
@ -76,13 +77,6 @@ fn local_fn() {
}), }),
r#"function(a,b)local function inner(c,d)end;return inner;end"# 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] #[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] #[test]
fn concat() { fn concat() {
assert_eq!(luaify!(concat!(a)), r#"a"#); assert_eq!(luaify!(concat!(a)), r#"a"#);
@ -402,3 +365,19 @@ fn length() {
r#"local a,b,c=#a,#b,#c;"# 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,3 +1,18 @@
//! Raw bindings for LuaJIT generated using [bindgen](https://docs.rs/bindgen/).
//!
//! ## Feature flags
//!
//! Features flags are used to control certain features of LuaJIT. They are not enabled by default
//! unless otherwise specified below.
//!
//! - `runtime`: links the target with `libluajit`. This should only be enabled by binary targets.
//! - `jit`: enables the JIT compiler. *This is enabled by default.*
//! - `ffi`: enables the FFI library. *This is enabled by default.*
//! - `unwind`: configures LuaJIT to use stack unwinding instead of `longjmp` for error handling.
//! **This MUST be enabled if `panic = "unwind"` is set.**
//! - `bundled-alloc`: configures LuaJIT to include its own bundled allocator. If this is not
//! enabled, LuaJIT will use the system allocator by default.
//! - `lua52`: enables Lua 5.2 compatibility mode. *This is enabled by default.*
#![allow(nonstandard_style)] #![allow(nonstandard_style)]
use std::{ffi::*, ptr}; use std::{ffi::*, ptr};

View File

@ -16,4 +16,3 @@ bitflags = { version = "2.9.1", features = ["std"] }
bstr = "1.12.0" bstr = "1.12.0"
luaffi = { path = "../luaffi" } luaffi = { path = "../luaffi" }
luajit-sys = { path = "../luajit-sys" } luajit-sys = { path = "../luajit-sys" }
thiserror = "2.0.12"

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,27 @@
//! luby standard library.
#[cfg(feature = "fs")]
pub use lb::fs; pub use lb::fs;
#[cfg(feature = "net")]
pub use lb::net; pub use lb::net;
#[cfg(feature = "task")]
pub use lb::task;
#[cfg(feature = "time")]
pub use lb::time;
#[cfg(feature = "sqlite")]
pub use lb_sqlite::sqlite;
#[doc(hidden)]
pub fn open(#[allow(unused)] rt: &mut lb::runtime::Builder) {
#[cfg(feature = "task")]
rt.module::<task::lb_tasklib>();
#[cfg(feature = "time")]
rt.module::<time::lb_timelib>();
#[cfg(feature = "fs")]
rt.module::<fs::lb_fslib>();
#[cfg(feature = "net")]
rt.module::<net::lb_netlib>();
#[cfg(feature = "sqlite")]
rt.module::<sqlite::lb_sqlitelib>();
}

View File

@ -1,7 +1,8 @@
use clap::Parser; use clap::Parser;
use luajit::Chunk;
use mimalloc::MiMalloc; use mimalloc::MiMalloc;
use owo_colors::OwoColorize; 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; use sysexits::ExitCode;
#[global_allocator] #[global_allocator]
@ -20,24 +21,42 @@ fn panic_cb(panic: &panic::PanicHookInfo) {
}; };
eprint!( eprint!(
"{}:\n{trace}", "{}\n{trace}",
format_args!( format_args!(
"thread '{}' panicked at {location}: {msg}", "thread '{}' panicked at {location}: {msg}",
thread::current().name().unwrap_or("<unnamed>") thread::current().name().unwrap_or("<unnamed>")
) )
.red() .red()
.bold()
); );
eprintln!( eprintln!(
"{}", "{}",
format_args!( format_args!(
"This is a bug in luby. Please kindly report this at {}.", "luby should never panic. Please kindly report this bug at {}.",
env!("CARGO_PKG_REPOSITORY") env!("CARGO_PKG_REPOSITORY")
) )
.yellow() .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)] #[derive(Debug, Parser)]
struct Args { struct Args {
/// Paths to scripts to execute. /// Paths to scripts to execute.
@ -60,12 +79,21 @@ struct Args {
#[clap(long, short = 'j', help_heading = "Runtime", value_name = "CMD=FLAGS")] #[clap(long, short = 'j', help_heading = "Runtime", value_name = "CMD=FLAGS")]
jit: Vec<String>, jit: Vec<String>,
/// Allow global variables.
#[clap(
long,
help_heading = "Runtime",
value_name = "ENABLED",
default_value_t = true
)]
allow_globals: bool,
/// Number of worker threads. /// Number of worker threads.
#[clap( #[clap(
long, long,
short = 'T', short = 'T',
help_heading = "Runtime", help_heading = "Runtime",
value_name = "THREADS", value_name = "COUNT",
default_value_t = Self::threads() default_value_t = Self::threads()
)] )]
threads: NonZero<usize>, threads: NonZero<usize>,
@ -74,16 +102,18 @@ struct Args {
#[clap( #[clap(
long, long,
help_heading = "Runtime", help_heading = "Runtime",
value_name = "THREADS", value_name = "COUNT",
default_value_t = Self::blocking_threads() default_value_t = Self::blocking_threads()
)] )]
blocking_threads: NonZero<usize>, blocking_threads: NonZero<usize>,
/// Enable tokio-console integration. /// Enable tokio-console integration.
#[clap(long, help_heading = "Debugging")] #[cfg(feature = "tokio-console")]
#[clap(long, help_heading = "Debugging", value_name = "ENABLED")]
enable_console: bool, enable_console: bool,
/// tokio-console publish address. /// tokio-console publish address.
#[cfg(feature = "tokio-console")]
#[clap( #[clap(
long, long,
help_heading = "Debugging", help_heading = "Debugging",
@ -91,7 +121,16 @@ struct Args {
default_value = "127.0.0.1:6669", default_value = "127.0.0.1:6669",
requires = "enable_console" 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. /// Print version.
#[clap(long, short = 'V')] #[clap(long, short = 'V')]
@ -108,19 +147,13 @@ impl Args {
} }
} }
fn exit_err<T, E: Display>(code: ExitCode) -> impl FnOnce(E) -> T { fn main() -> ExitCode {
move |err| {
eprintln!("{}", err.red());
code.exit()
}
}
fn main() -> Result<(), ExitCode> {
panic::set_hook(Box::new(panic_cb)); panic::set_hook(Box::new(panic_cb));
let args = Args::parse(); let args = Args::parse();
if args.version { if args.version {
return Ok(print_version()); print_version();
return ExitCode::Ok;
} }
init_logger(&args); init_logger(&args);
@ -131,7 +164,10 @@ fn main() -> Result<(), ExitCode> {
tokio.block_on(async { tokio.block_on(async {
lua.await; lua.await;
main.await.unwrap() match main.await {
Ok(res) => res,
Err(err) => panic::resume_unwind(err.into_panic()),
}
}) })
} }
@ -149,7 +185,7 @@ fn print_version() {
fn init_logger(args: &Args) { fn init_logger(args: &Args) {
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use tracing_subscriber::{Layer, util::*}; use tracing_subscriber::util::*;
let log = tracing_subscriber::fmt() let log = tracing_subscriber::fmt()
.compact() .compact()
@ -163,98 +199,68 @@ fn init_logger(args: &Args) {
.with_target(false) .with_target(false)
.finish(); .finish();
if args.enable_console { #[cfg(feature = "tokio-console")]
{
use tracing_subscriber::Layer;
console_subscriber::ConsoleLayer::builder() console_subscriber::ConsoleLayer::builder()
.with_default_env() .with_default_env()
.server_addr(args.console_addr) .server_addr(args.console_addr)
.spawn() .spawn()
.with_subscriber(log) .with_subscriber(log)
.init() .init();
} else {
log.init()
} }
#[cfg(not(feature = "tokio-console"))]
log.init();
} }
fn init_tokio(args: &Args) -> tokio::runtime::Runtime { 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(), 1 => tokio::runtime::Builder::new_current_thread(),
n => { n => {
let mut rt = tokio::runtime::Builder::new_multi_thread(); let mut rt = tokio::runtime::Builder::new_multi_thread();
rt.worker_threads(n - 1); rt.worker_threads(n - 1);
rt rt
} }
}; }
.enable_all()
rt.enable_all()
.thread_name("luby") .thread_name("luby")
.max_blocking_threads(args.blocking_threads.get()) .max_blocking_threads(args.blocking_threads.get())
.build() .build()
.unwrap_or_else(exit_err(ExitCode::OsErr)) .unwrap_or_else(unwrap_exit(ExitCode::OsErr))
} }
fn init_lua(args: &Args) -> lb::runtime::Runtime { fn init_lua(args: &Args) -> lb::runtime::Runtime {
let rt = lb::runtime::Builder::new(); let mut rt = lb::runtime::Builder::new();
let mut rt = rt.build().unwrap_or_else(exit_err(ExitCode::Software)); luby::open(&mut rt);
for arg in args.jit.iter() { if args.dump.iter().find(|s| *s == "cdef").is_some() {
let mut s = rt.guard(); print!("{}", rt.registry()); // for cdef debugging
if let Some((cmd, flags)) = parse_jitlib_cmd(arg)
&& let Ok(_) = s.require(format!("jit.{cmd}"), 1)
{
(s.push("start"), s.get(-2), s.push(flags));
s.call(1, 0)
} else {
s.require("jit", 1).unwrap();
match arg.as_str() {
cmd @ ("on" | "off" | "flush") => {
(s.push(cmd), s.get(-2));
s.call(0, 0)
}
arg => {
(s.push("opt"), s.get(-2));
(s.push("start"), s.get(-2), s.push(arg));
s.call(1, 0)
}
}
}
.unwrap_or_else(exit_err(ExitCode::Usage));
} }
rt 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)> { async fn main_async(args: Args, cx: &mut lb::runtime::Context) -> ExitCode {
match s {
"p" => Some(("p", "Flspv10")),
"v" => Some(("v", "-")),
"dump" => Some(("dump", "tirs")),
_ => s.split_once('='),
}
}
async fn main_async(args: Args, state: &mut luajit::State) -> Result<(), ExitCode> {
for ref path in args.path { for ref path in args.path {
let mut s = state.guard();
let chunk = match std::fs::read(path) { let chunk = match std::fs::read(path) {
Ok(chunk) => chunk, Ok(chunk) => chunk,
Err(err) => { Err(err) => {
eprintln!("{}", format_args!("{path}: {err}").red()); eprintln!("{}", format_args!("{path}: {err}").red().bold());
ExitCode::NoInput.exit(); return ExitCode::NoInput;
} }
}; };
s.load(&luajit::Chunk::new(chunk).path(path)) if let Err(ref err) = cx.load(&Chunk::new(chunk).with_path(path)) {
.unwrap_or_else(exit_err(ExitCode::NoInput)); cx.report_error(err);
} else if let Err(ref err) = cx.call_async(0, Some(0)).await {
if let Err(err) = s.call_async(0, 0).await { cx.report_error(err);
match err.trace() {
Some(trace) => eprintln!("{}\n{trace}", err.red()), // runtime error
None => eprintln!("{}", err.red()),
}
ExitCode::DataErr.exit();
} }
} }
Ok(()) ExitCode::Ok
} }

View File

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

188
tests/main.lua Normal file
View File

@ -0,0 +1,188 @@
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
faint = "\x1b[2;39;49m", -- faint
}
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 rjust(s, w)
if w == nil then w = 12 end
if #s >= w then return s end
return (" "):rep(w - #s) .. s
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, trace = xpcall(test.f, trace, test)
if ok then
test.state = "pass"
print(("%s %s"):format(style("pass", rjust("PASS")), name_test(test)))
else
test.state = "fail"
print(("%s %s\n\n%s\n"):format(style("fail", rjust("!!! FAIL")), name_test(test), trace))
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_secs()
local retcode
if fail == 0 then
print(style("pass", ("\t%s %d tests passed"):format(icon.check, pass)))
retcode = 0
else
print(
("\t%s, %s"):format(
style("pass", ("%s %d tests passed"):format(icon.check, pass)),
style("fail", ("%s %d tests failed"):format(icon.cross, fail))
)
)
retcode = 1
end
print(style("faint", ("\t%s completed in %.2fs"):format(icon.chevron, elapsed)))
cx = nil
collectgarbage()
check_refs() -- check that all refs were properly unref'ed in destructors
return retcode -- 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
local function include_doctest(path, pat)
for entry in fs.glob_dir(path, pat) do
local line, doctest = 0, nil
for s in fs.read(entry:path()):gmatch("([^\n]*)\n?") do
line = line + 1
local prefix = s:match("^%s*///")
s = prefix and s:sub(#prefix + 1)
if s and not s:match("^%s*```%s*$") then
if s:match("^%s*```lua$") then
doctest = { line = line, col = #prefix + 2 }
elseif doctest then
table.insert(doctest, (" "):rep(#prefix) .. s)
end
else
if doctest then
local name = ("%s:%d:%d"):format(entry:path(), doctest.line, doctest.col)
local f, err = loadstring(table.concat(doctest, "\n"), "@" .. name)
if not f then error(err) end
test(("%s %s"):format(name, style("faint", "(doctest)")), f)
end
doctest = nil
end
end
end
end
include("tests", "**/*.lua")
include("crates", "*/tests/**/*.lua")
include_doctest("src", "**/*.rs")
include_doctest("crates", "*/src/**/*.rs")
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);
}