Skip to content
dream-path
guide

Embedding Luau

Give scripts @dream/path through l3i: the feature, the toolchain, composing the extension, the global, and type definitions.

With the lua feature, dream-path is an l3i extension: it describes the @dream/path module, and a Rust host that runs Luau through l3i composes it into its runtime. Scripts then get the same rules as Rust, byte for byte. The crate never creates a VM and never installs a global; both are the host’s decisions.

Dependencies and toolchain#

[dependencies]
dream-path = { version = "0.3", features = ["lua"] }
l3i = "0.1"

l3i builds Luau itself, and only with clang, lld and cross-language thin LTO: its build script refuses any other configuration and names the missing piece. Cargo does not pass a dependency’s configuration on, so the host copies the policy into its own .cargo/config.toml, as this crate does:

[env]
CXX = "clang++"

[target.x86_64-unknown-linux-gnu]
rustflags = ["-Clinker-plugin-lto", "-Clinker=clang", "-Clink-arg=-fuse-ld=lld"]

clang and rustc must use the same LLVM major version. The l3i toolchain notes have the lines for macOS and Windows, and the measurements behind the rule.

Composing the extension#

PathExtension goes into the host’s RuntimePlan, next to whatever else the host provides. Every runtime made from the plan can require("@dream/path"):

use dream_path::lua::PathExtension;
use l3i::Runtime;
use l3i::extension::RuntimePlan;

fn main() -> l3i::Result<()> {
    let plan = RuntimePlan::builder().extension(PathExtension).finalize()?;
    let runtime = Runtime::from_plan(&plan)?;

    runtime.exec(r#"
        local dreamPath = require("@dream/path")
        assert(dreamPath.normalize([[Textures\Tx_Wood_01.DDS]]) == "textures/tx_wood_01.dds")
    "#)
}

The dreamPath global#

Scripts written for 0.2 and earlier used a dreamPath global. A host that still wants one exposes the module as a compatibility global through its policy; the global and require then return the same table:

use dream_path::lua::{MODULE, MODULE_NAME, PathExtension};
use l3i::Runtime;
use l3i::extension::{RuntimePlan, RuntimePolicy};

fn main() -> l3i::Result<()> {
    let policy = RuntimePolicy::new().compat_global(MODULE, MODULE_NAME);
    let plan = RuntimePlan::builder().policy(policy).extension(PathExtension).finalize()?;
    let runtime = Runtime::from_plan(&plan)?;

    runtime.exec(r#"assert(dreamPath == require("@dream/path"))"#)
}

MODULE is "@dream/path" and MODULE_NAME is "dreamPath".

Types for editors and checks#

Every function carries a Luau signature. plan.type_definitions() returns the .d.luau text for everything in the plan, @dream/path included, ready to save for an editor’s language server. With l3i’s analysis feature, plan.check_definitions() type-checks those definitions; this crate’s tests run it, and type-check a strict script against the module, behind the luau-analysis feature (cargo test --features luau-analysis), so a plain test run does not build the analysis frontend.

What scripts get#

  • Byte strings. A path argument is read as a borrowed view of the Luau string: no copy, no UTF-8 requirement. Results can contain invalid UTF-8 and NUL bytes, so a C or C++ host must use length-aware Lua APIs.
  • Strings only. A missing or non-string argument is an error, numbers included; a missing path component is nil.
  • A frozen module. Scripts cannot add to or replace its functions.
  • No allocation after warm-up. Each call normalizes once, into a buffer per thread, and pushes the result from there; a path that is already normalized is pushed as it is.

The Luau API lists the functions.

From 0.2#

lua::create_module, lua::register_module, lua::register_module_as and the standalone-lua feature are gone, with mlua. Compose PathExtension as above; the functions, their names and their results are unchanged.