├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── examples
├── .cargo
│ └── config.toml
├── 01-hello-autput
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── README.md
│ ├── example.project.json
│ ├── lune
│ │ └── build-this-example.luau
│ ├── src-luau
│ │ └── init.server.luau
│ └── src-rust
│ │ └── main.rs
├── README.md
└── build-examples
│ └── build_single_example.luau
├── gh-assets
└── autput.svg
├── luau
└── autput.luau
├── rust
├── Cargo.lock
├── Cargo.toml
└── src
│ └── lib.rs
└── selene.toml
/.gitignore:
--------------------------------------------------------------------------------
1 | **/target
2 | **/target-luau
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "autput"
7 | version = "0.1.0"
8 | dependencies = [
9 | "log",
10 | ]
11 |
12 | [[package]]
13 | name = "hello-autput"
14 | version = "0.1.0"
15 | dependencies = [
16 | "autput",
17 | "log",
18 | ]
19 |
20 | [[package]]
21 | name = "log"
22 | version = "0.4.21"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
25 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "rust",
4 | "examples/01-hello-autput"
5 | ]
6 | resolver = "2"
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License + Security Disclaimer
2 |
3 | Copyright (c) 2025, Daniel P H Fox
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
30 | **SECURITY DISCLAIMER:** THE USER OF THIS SOFTWARE IS SOLELY RESPONSIBLE FOR
31 | CONDUCTING SECURITY AUDITS, REVIEWS, AND ENSURING THE SOFTWARE'S SAFE DEPLOYMENT.
32 | THE COPYRIGHT HOLDER AND CONTRIBUTORS DISCLAIM ANY RESPONSIBILITY OR LIABILITY
33 | FOR SECURITY VULNERABILITIES, DATA BREACHES, SUPPLY-CHAIN ATTACKS, OR ANY OTHER
34 | SECURITY-RELATED ISSUES RESULTING FROM THE USE OF THIS SOFTWARE OR ITS DEPENDENCIES.
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Log Rust prints and panics to Luau for easy debugging.
6 |
7 |
8 | ## Install
9 |
10 | ```
11 | cargo add autput
12 | ```
13 |
14 | ## Brief
15 |
16 | When embedding Rust in Luau via Wasynth, there's no good default error handler
17 | or printing functionality. Rust panics cause mysterious error messages and
18 | printed messages go nowhere.
19 |
20 | Autput aims to be a minimal useful product for sending panics and prints to Luau
21 | from Rust. You need only initialise Autput, and all the relevant panic handling
22 | and printing features are turned on for you automatically, so you can get to
23 | work right away.
24 |
25 | ```Rust
26 | fn main() {
27 | autput::connect();
28 | info!("This is an info message.");
29 | warn!("This is a warning message.");
30 | error!("This is an error message.");
31 | panic!("This is a panic.");
32 | }
33 | ```
34 |
35 | - Compatible with the `log` crate
36 | - Send to `print()`, `warn()` (Roblox only), or your own Luau log functions
37 | - Rust panics are redirected to Luau's `error()`
38 |
39 | ## A note about `println!`
40 |
41 | It isn't possible to redirect where standard output goes in Rust. As a result,
42 | Autput can't restore `println!()` functionality, or other similar hardcoded
43 | printing procedures.
44 |
45 | Instead, you should use the `log` crate and its macros: `info!()`, `warn!()`
46 | etc.
47 |
48 | ## License
49 |
50 | Licensed the same way as all of my open source projects: BSD 3-Clause + Security Disclaimer.
51 |
52 | As with all other projects, you accept responsibility for choosing and using this project.
53 |
54 | See [LICENSE](./LICENSE) or [the license summary](https://github.com/dphfox/licence) for details.
55 |
--------------------------------------------------------------------------------
/examples/.cargo/config.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | target = "wasm32-unknown-unknown"
--------------------------------------------------------------------------------
/examples/01-hello-autput/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "hello-lingua"
7 | version = "0.1.0"
8 | dependencies = [
9 | "lingua",
10 | "serde",
11 | ]
12 |
13 | [[package]]
14 | name = "itoa"
15 | version = "1.0.11"
16 | source = "registry+https://github.com/rust-lang/crates.io-index"
17 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
18 |
19 | [[package]]
20 | name = "lingua"
21 | version = "0.1.0"
22 | dependencies = [
23 | "log",
24 | "serde",
25 | "serde_json",
26 | "thiserror",
27 | ]
28 |
29 | [[package]]
30 | name = "log"
31 | version = "0.4.21"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
34 |
35 | [[package]]
36 | name = "proc-macro2"
37 | version = "1.0.85"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
40 | dependencies = [
41 | "unicode-ident",
42 | ]
43 |
44 | [[package]]
45 | name = "quote"
46 | version = "1.0.36"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
49 | dependencies = [
50 | "proc-macro2",
51 | ]
52 |
53 | [[package]]
54 | name = "ryu"
55 | version = "1.0.18"
56 | source = "registry+https://github.com/rust-lang/crates.io-index"
57 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
58 |
59 | [[package]]
60 | name = "serde"
61 | version = "1.0.203"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
64 | dependencies = [
65 | "serde_derive",
66 | ]
67 |
68 | [[package]]
69 | name = "serde_derive"
70 | version = "1.0.203"
71 | source = "registry+https://github.com/rust-lang/crates.io-index"
72 | checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
73 | dependencies = [
74 | "proc-macro2",
75 | "quote",
76 | "syn",
77 | ]
78 |
79 | [[package]]
80 | name = "serde_json"
81 | version = "1.0.117"
82 | source = "registry+https://github.com/rust-lang/crates.io-index"
83 | checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
84 | dependencies = [
85 | "itoa",
86 | "ryu",
87 | "serde",
88 | ]
89 |
90 | [[package]]
91 | name = "syn"
92 | version = "2.0.66"
93 | source = "registry+https://github.com/rust-lang/crates.io-index"
94 | checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
95 | dependencies = [
96 | "proc-macro2",
97 | "quote",
98 | "unicode-ident",
99 | ]
100 |
101 | [[package]]
102 | name = "thiserror"
103 | version = "1.0.61"
104 | source = "registry+https://github.com/rust-lang/crates.io-index"
105 | checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
106 | dependencies = [
107 | "thiserror-impl",
108 | ]
109 |
110 | [[package]]
111 | name = "thiserror-impl"
112 | version = "1.0.61"
113 | source = "registry+https://github.com/rust-lang/crates.io-index"
114 | checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
115 | dependencies = [
116 | "proc-macro2",
117 | "quote",
118 | "syn",
119 | ]
120 |
121 | [[package]]
122 | name = "unicode-ident"
123 | version = "1.0.12"
124 | source = "registry+https://github.com/rust-lang/crates.io-index"
125 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
126 |
--------------------------------------------------------------------------------
/examples/01-hello-autput/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "hello-autput"
3 | version = "0.1.0"
4 | edition = "2021"
5 | publish = false
6 |
7 | [[bin]]
8 | name = "example_wasm"
9 | path = "src-rust/main.rs"
10 |
11 | [dependencies]
12 | autput = { path = "../../rust" }
13 | log = "0.4.21"
14 |
--------------------------------------------------------------------------------
/examples/01-hello-autput/README.md:
--------------------------------------------------------------------------------
1 | # 01 - Hello Autput
2 |
3 | **Initialising Autput minimally with out-of-the-box settings.** You can try using this example as a template for your
4 | own experimentation.
5 |
6 | ## Author comments
7 |
8 | If you have a well-defined entry point like `main()`, you can use `autput::connect()` in there, and Autput will connect
9 | to the Luau side for you. By default, the maximum logged level is `Info`.
--------------------------------------------------------------------------------
/examples/01-hello-autput/example.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Hello Autput",
3 | "tree": {
4 | "$className": "DataModel",
5 |
6 | "ServerScriptService": {
7 | "$className": "ServerScriptService",
8 |
9 | "hello_autput": {
10 | "$className": "Folder",
11 |
12 | "src_luau": {
13 | "$path": "src-luau"
14 | },
15 |
16 | "target_luau": {
17 | "$className": "Folder",
18 |
19 | "example_wasm": {
20 | "$path": "target-luau/example_wasm.luau"
21 | }
22 | },
23 |
24 | "autput": {
25 | "$path": "../../luau/autput.luau"
26 | }
27 | }
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/examples/01-hello-autput/lune/build-this-example.luau:
--------------------------------------------------------------------------------
1 | --!nocheck
2 | --selene: allow(incorrect_standard_library_use)
3 | local build_single_example = require("../../build-examples/build_single_example")
4 |
5 | build_single_example("01-hello-autput", "bin")
--------------------------------------------------------------------------------
/examples/01-hello-autput/src-luau/init.server.luau:
--------------------------------------------------------------------------------
1 | --!nocheck
2 | -- See src/main.rs for an introduction to this example.
3 |
4 | local autput = require(script.Parent.autput)
5 | local example_wasm_loader = require(script.Parent.target_luau.example_wasm)
6 |
7 | local wasm_env = { func_list = {} }
8 | local finish_autput_init = autput.init(wasm_env)
9 | local example_wasm_module = example_wasm_loader({env = wasm_env})
10 | finish_autput_init(example_wasm_module)
11 |
12 | example_wasm_module.func_list.main()
--------------------------------------------------------------------------------
/examples/01-hello-autput/src-rust/main.rs:
--------------------------------------------------------------------------------
1 | #[cfg(not(target_arch = "wasm32"))]
2 | compile_error!("This project must target WebAssembly to compile correctly.");
3 |
4 | use log::{debug, error, info, trace, warn};
5 |
6 | fn main() {
7 | autput::connect();
8 | info!("This is an info message.");
9 | warn!("This is a warning message.");
10 | error!("This is an error message.");
11 | panic!("This is a panic.");
12 | }
13 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # How to run examples
2 |
3 | ## Prerequisites
4 |
5 | You will need `lune`, `cargo`, `wasm2luau` and `rojo` installed.
6 |
7 | *Tip: `wasm2luau` can be installed with this command:*
8 |
9 | ```
10 | cargo install --git https://github.com/Rerumu/Wasynth codegen-luau
11 | ```
12 |
13 | [See Wasynth on GitHub for more details.](https://github.com/Rerumu/Wasynth/wiki/From-Rust,-to-Lua)
14 |
15 | ## Steps
16 |
17 | - In a terminal, `cd` into the folder of the example you want to run
18 | - Enter `lune run build-this-example`
19 | - Open the `build.rbxl` that was set up inside `target-luau`.
20 | - In Roblox Studio, choose 'Run' (you do not need to 'Play' the file)
--------------------------------------------------------------------------------
/examples/build-examples/build_single_example.luau:
--------------------------------------------------------------------------------
1 | -- Lune-style imports make both Luau LSP and Selene very unhappy...
2 | --!nocheck
3 | --!nolint LocalShadow
4 | --selene: allow(incorrect_standard_library_use)
5 | local process = require("@lune/process")
6 | --selene: allow(incorrect_standard_library_use)
7 | local stdio = require("@lune/stdio")
8 | --selene: allow(incorrect_standard_library_use)
9 | local fs = require("@lune/fs")
10 |
11 | local RED = stdio.color("red")
12 | local BLUE = stdio.color("blue")
13 | local YELLOW = stdio.color("yellow")
14 | local MAGENTA = stdio.color("magenta")
15 | local RESET = stdio.color("reset")
16 |
17 | local function get_example_dir(
18 | example_name: string
19 | ): string
20 | return `../{example_name}`
21 | end
22 |
23 | local function build_single_example(
24 | example_name: string,
25 | _example_type: "lib" | "bin"
26 | ): ()
27 | local working_dir = get_example_dir(example_name)
28 | print(`{YELLOW}[Build Example]{RESET} Building example: {BLUE}{example_name}{RESET}`)
29 |
30 | -- Compile Rust source code to WASM
31 | do
32 | print(`{BLUE}[Build Example > WASM]{RESET} Compiling WASM module with cargo...`)
33 | local result = process.spawn(
34 | "cargo",
35 | {
36 | "build",
37 | "--release",
38 | "--target-dir",
39 | "target"
40 | },
41 | {
42 | cwd = working_dir
43 | }
44 | )
45 | if not result.ok then
46 | print(result.stdout)
47 | print(result.stderr)
48 | print(`{BLUE}[Build Example > WASM]{RESET} WASM compile failed!`)
49 | return
50 | end
51 | print(`{BLUE}[Build Example > WASM]{RESET} WASM compile successful!`)
52 | end
53 |
54 | -- Convert WASM to Luau with Wasynth
55 | do
56 | print(`{MAGENTA}[Build Example > Wasynth]{RESET} Transpiling WASM to Luau with Wasynth...`)
57 | local result = process.spawn(
58 | "wasm2luau",
59 | {
60 | "target/wasm32-unknown-unknown/release/example_wasm.wasm"
61 | },
62 | {
63 | cwd = working_dir
64 | }
65 | )
66 | if not result.ok then
67 | print(result.stdout)
68 | print(result.stderr)
69 | print(`{MAGENTA}[Build Example > Wasynth]{RESET} Transpilation failed!`)
70 | return
71 | end
72 |
73 | print(`{MAGENTA}[Build Example > Wasynth]{RESET} Saving transpilation to file...`)
74 |
75 | pcall(
76 | fs.writeDir,
77 | working_dir .. "/target-luau"
78 | )
79 | local ok, result = pcall(
80 | fs.writeFile,
81 | working_dir .. "/target-luau/example_wasm.luau",
82 | "--!nocheck\n" .. result.stdout
83 | )
84 | if not ok then
85 | print(tostring(result))
86 | print(`{MAGENTA}[Build Example > Wasynth]{RESET} Transpilation failed!`)
87 | return
88 | end
89 |
90 | print(`{MAGENTA}[Build Example > Wasynth]{RESET} Transpilation succesful!`)
91 | end
92 |
93 | -- Build Rojo project
94 | do
95 | print(`{RED}[Build Example > Rojo]{RESET} Building Roblox project with Rojo...`)
96 | local result = process.spawn(
97 | "rojo",
98 | {
99 | "build",
100 | "example.project.json",
101 | "--output",
102 | "target-luau/build.rbxl"
103 | },
104 | {
105 | cwd = working_dir
106 | }
107 | )
108 | if not result.ok then
109 | print(result.stdout)
110 | print(result.stderr)
111 | print(`{RED}[Build Example > Rojo]{RESET} Rojo build failed!`)
112 | return
113 | end
114 | print(`{RED}[Build Example > Rojo]{RESET} Rojo build successful!`)
115 | end
116 |
117 | print(`{YELLOW}[Build Example]{RESET} Build finished!`)
118 | print(`{YELLOW}[Build Example]{RESET} The example project has been set up inside {BLUE}target-luau/build.rbxl{RESET} for you.`)
119 | print(`{YELLOW}[Build Example]{RESET} You can continuously serve this example by running {BLUE}rojo serve example.project.json{RESET}.`)
120 | end
121 |
122 | return build_single_example
--------------------------------------------------------------------------------
/gh-assets/autput.svg:
--------------------------------------------------------------------------------
1 |
37 |
--------------------------------------------------------------------------------
/luau/autput.luau:
--------------------------------------------------------------------------------
1 | --!strict
2 | --Licensed under BSD3 from Autput, (c) Daniel P H Fox 2025
3 |
4 | --------------------------------------------------------------------------------
5 | -- TYPE DEFINITIONS ------------------------------------------------------------
6 | --------------------------------------------------------------------------------
7 |
8 | -- Type aliases for convenience and documentation
9 | type ptr_const_u8 = number
10 | type u32 = number
11 | type u8 = number
12 |
13 | -- Passed to Wasynth when initialising a WebAssembly module.
14 | -- Autput injects some of its own members into this environment, but otherwise
15 | -- does not care about the rest of the contents.
16 | type WasmEnvironment = {
17 | func_list: {
18 | [string]: (...any) -> (...any)
19 | }
20 | }
21 |
22 | -- Returned by Wasynth when initialising a WebAssembly module.
23 | -- Autput expects certain members to be present.
24 | type WasmModule = {
25 | rt: {
26 | load: {
27 | string: (
28 | memory: WasmMemory,
29 | ptr: ptr_const_u8,
30 | len: u32
31 | ) -> string
32 | }
33 | },
34 | memory_list: {
35 | memory: WasmMemory
36 | }
37 | }
38 |
39 | -- An entry in the `memory_list` dictionary returned by Wasynth.
40 | -- Autput doesn't care about its specific type - it's treated opaquely.
41 | type WasmMemory = unknown
42 |
43 | -- The internal state used by Autput's Luau-side API.
44 | -- This is not shared between WASM modules.
45 | type ApiState = {
46 | -- This module provides the low level FFI that the API functions use.
47 | module: WasmModule,
48 | log_fn: LogFn
49 | }
50 |
51 | -- The public API surface exposed by Autput after initialisation.
52 | -- The API state is expected to be encapsulated as upvalues in closures.
53 | export type Api = {
54 | -- Change where Rust logs are sent to, returning the old handler. This does
55 | -- not affect panic handling.
56 | replace_log_fn: (
57 | new_log_fn: LogFn
58 | ) -> LogFn,
59 | }
60 |
61 |
62 | export type LogLevel = "error" | "warn" | "info" | "debug" | "trace"
63 | export type LogFn = (
64 | log_level: LogLevel,
65 | message: string
66 | ) -> ()
67 |
68 | --------------------------------------------------------------------------------
69 | -- CONSTANTS -------------------------------------------------------------------
70 | --------------------------------------------------------------------------------
71 |
72 | local LOG_LEVELS: {LogLevel} = {
73 | "error",
74 | "warn",
75 | "info",
76 | "debug",
77 | "trace"
78 | }
79 |
80 | local STD_LOG: LogFn =
81 | if typeof(warn) == "function" then
82 | function(log_level, message)
83 | if log_level == "error" or log_level == "warn" then
84 | warn(message)
85 | else
86 | print(message)
87 | end
88 | end
89 | else
90 | function(_, message)
91 | print(message)
92 | end
93 |
94 | --------------------------------------------------------------------------------
95 | -- FOREIGN FUNCTION INTERFACE --------------------------------------------------
96 | --------------------------------------------------------------------------------
97 |
98 | local extern_fn = {}
99 |
100 | function extern_fn.log(
101 | api_state: ApiState,
102 | level_ord: u8,
103 | ptr: ptr_const_u8,
104 | len: u32
105 | ): ()
106 | local message = api_state.module.rt.load.string(
107 | api_state.module.memory_list.memory,
108 | ptr,
109 | len
110 | )
111 | local log_level = LOG_LEVELS[level_ord] or error(
112 | `[autput] unknown log level{level_ord} for: {message}`, 0
113 | )
114 | api_state.log_fn(log_level :: LogLevel, message)
115 | end
116 |
117 | function extern_fn.panic(
118 | api_state: ApiState,
119 | ptr: ptr_const_u8,
120 | len: u32
121 | ): ()
122 | local message = api_state.module.rt.load.string(
123 | api_state.module.memory_list.memory,
124 | ptr,
125 | len
126 | )
127 | error(message, 0)
128 | end
129 |
130 | --------------------------------------------------------------------------------
131 | -- LUAU SIDE API ---------------------------------------------------------------
132 | --------------------------------------------------------------------------------
133 |
134 | local api_fn = {}
135 |
136 | -- See the `Api` type for user-facing documentation.
137 | function api_fn.replace_log_fn(
138 | api_state: ApiState,
139 | new_log_fn: LogFn
140 | ): LogFn
141 | local old_log_fn = api_state.log_fn
142 | api_state.log_fn = new_log_fn
143 | return old_log_fn
144 | end
145 |
146 | --------------------------------------------------------------------------------
147 | -- INITIALISATION --------------------------------------------------------------
148 | --------------------------------------------------------------------------------
149 |
150 | local autput = {}
151 |
152 | -- Autput needs to register its own extern functions into the WASM environment,
153 | -- and it needs to obtain a reference to the WASM module so that it can invoke
154 | -- functions that exist in the WASM runtime. Once both have been received, you
155 | -- will receive an `Api` that you can use with the provided module.
156 | function autput.init(
157 | env: WasmEnvironment
158 | )
159 | local api_state: ApiState? = nil
160 | for fn_name, fn in pairs(extern_fn) do
161 | env.func_list["autput_"..fn_name] = function(...)
162 | if api_state == nil then
163 | print("[autput] api not initialised yet on luau side - cannot handle incoming data")
164 | return
165 | end
166 | return fn(api_state, ...)
167 | end
168 | end
169 | return function(
170 | module: WasmModule
171 | ): Api
172 | assert(api_state == nil, "[autput] only one module allowed per environment")
173 | api_state = {
174 | module = module,
175 | log_fn = STD_LOG
176 | }
177 | local api = {}
178 | for fn_name, fn in pairs(api_fn) do
179 | api[fn_name] = function(...)
180 | return fn(api_state, ...)
181 | end
182 | end
183 | return api :: any
184 | end
185 | end
186 |
187 | return autput
188 |
--------------------------------------------------------------------------------
/rust/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "rust"
7 | version = "0.1.0"
8 |
--------------------------------------------------------------------------------
/rust/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "autput"
3 | version = "0.1.0"
4 | authors = ["Daniel P H Fox "]
5 | edition = "2021"
6 | description = "Log Rust prints and panics to Luau for easy debugging."
7 | repository = "https://github.com/dphfox/autput"
8 | license = "MIT"
9 | keywords = ["roblox", "wasm", "wasynth", "lua", "luau"]
10 | categories = ["wasm", "development-tools::debugging"]
11 |
12 | [lib]
13 | name = "autput"
14 |
15 | [dependencies]
16 | log = { version = "0.4.21", features = ["std"] }
17 |
--------------------------------------------------------------------------------
/rust/src/lib.rs:
--------------------------------------------------------------------------------
1 | use std::{panic, cell::RefCell};
2 |
3 | use log::{Level, LevelFilter, Log, SetLoggerError};
4 |
5 | pub const LOG_LEVEL_FILTER: LevelFilter = log::STATIC_MAX_LEVEL;
6 |
7 | extern "C" {
8 | fn autput_log(
9 | level: u8,
10 | ptr: *const u8,
11 | len: u32
12 | );
13 |
14 | fn autput_panic(
15 | ptr: *const u8,
16 | len: u32
17 | );
18 | }
19 |
20 | fn set_panic_hook() {
21 | panic::set_hook(
22 | Box::new(|panic| {
23 | let message = format!("{panic}");
24 | unsafe {
25 | autput_panic(
26 | message.as_ptr(),
27 | message.len() as u32
28 | );
29 | }
30 | })
31 | )
32 | }
33 |
34 | pub fn connect_with(
35 | logger: Autput
36 | ) {
37 | self::set_panic_hook();
38 | logger.connect().expect("There was an error connecting Autput to `log` on the Rust side.");
39 | }
40 |
41 | pub fn connect_once_with Autput>(
42 | make_logger: T
43 | ) {
44 | thread_local! {
45 | static CONNECTED: RefCell = RefCell::new(false);
46 | }
47 | CONNECTED.with_borrow_mut(|is_connected| {
48 | if *is_connected {
49 | return;
50 | }
51 | connect_with(make_logger());
52 | *is_connected = true;
53 | })
54 | }
55 |
56 | pub fn connect() {
57 | connect_with(Autput::default())
58 | }
59 |
60 | pub fn connect_once() {
61 | connect_once_with(Autput::default)
62 | }
63 |
64 | pub struct Autput {
65 | pub max_level: LevelFilter
66 | }
67 |
68 | impl Autput {
69 | fn connect(self) -> Result<(), SetLoggerError> {
70 | log::set_max_level(self.max_level);
71 | log::set_boxed_logger(Box::new(self))
72 | }
73 | }
74 |
75 | impl Default for Autput {
76 | fn default() -> Self {
77 | Self {
78 | max_level: LevelFilter::Info
79 | }
80 | }
81 | }
82 |
83 | impl Log for Autput {
84 | fn enabled(&self, metadata: &log::Metadata) -> bool {
85 | metadata.level() <= self.max_level
86 | }
87 |
88 | fn log(&self, record: &log::Record) {
89 | if !self.enabled(record.metadata()) {
90 | return;
91 | }
92 | let level_string = format!("{:<5}", record.level().to_string());
93 | let target = if !record.target().is_empty() {
94 | record.target()
95 | } else {
96 | record.module_path().unwrap_or_default()
97 | };
98 | let message = format!("{} [{}] {}", level_string, target, record.args());
99 | unsafe {
100 | autput_log(
101 | record.level() as u8,
102 | message.as_ptr(),
103 | message.len() as u32
104 | );
105 | }
106 | }
107 |
108 | fn flush(&self) {}
109 | }
--------------------------------------------------------------------------------
/selene.toml:
--------------------------------------------------------------------------------
1 | std = "roblox"
2 |
3 | [lints]
4 | shadowing = "allow"
--------------------------------------------------------------------------------