├── .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 | Autput logo 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 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" --------------------------------------------------------------------------------