Luau Web | Luau bindings with dynamic JavaScript interop for web browsers, Node.js, and TypeScript

Luau Web 1.1.0 @690

Luau bindings with dynamic JavaScript interop for web browsers, Node.js, and TypeScript.



Luau Web [1.1.0@690] — Javascript bindings for Luau
This project uses luau-interop which is my fork of Luau with a rewritten WASM execution API.

Features

  • Easy to use and you won’t have to call Luau C API functions yourself
  • Create and override globals in the Luau runtime
  • Luau can interact with all values from JS
  • JS can interact with all values from Luau
  • Embeddable in websites, and Node.JS without extra setup.
  • Sandboxed environment, basically bulletproof
  • Near-native performance (around 10% performance loss, still around 40M operations/sec, around the same as Luau’s demo Demo - Luau if not faster)

Security

The Luau runtime is safe and secure by design, and my fork preserves that safety.
The security is in your hands and you have to make sure the values and functions you expose to the runtime are also secure. You can follow the basic security principles in the wiki.

You could give the runtime access to a node library like fs or any other, but only do it if you are only running your own trusted code, or if you heavily safeguard the exposed functions.

Risky Example
import FS from "fs";
import { LuauState, InternalLuauWasmModule } from "luau-web"

InternalLuauWasmModule.onRuntimeInitialized = () => {
	const state = new LuauState({
		FS
	});

	const func = state.loadstring(`print(FS.readdirSync('.'))`, "index.ts", true);
	func();
};

For more info see luau-web/wiki/Security

Example Script

import { LuauState, InternalLuauWasmModule, LuauTable, LuauFunction } from "luau-web"

InternalLuauWasmModule.onRuntimeInitialized = () => {
	const state = new LuauState({
		example: function (str1: string, tbl: LuauTable, luaPrint: LuauFunction) {
			luaPrint(tbl, "hi :3");
			return str1 + tbl.one;
		}
	});

	const func = state.loadstring(`print(example(..., {one = 2}, print)); return ...`, "index.ts", true); // prints "hello2"
	console.log(func("hello")); // returns ["hello"]
};

It interacts and integrates cleanly with Luau values.

Limitations

  • It should be pretty stable, but this is still new - report issues you encounter and I will fix them as best as I can.
  • Some features, like full Luau value APIs, are still missing but will be added in a future update (most importantly buffer operations like buffer.readu16 to be called on Luau value references)
21 Likes

imagine if someone wrote a web app in luau with this

crazy stuff! :face_holding_back_tears:

1 Like
Release 1.1.0 @690
Install using (pnpm, npm) install luau-web

Updates

  • ✓ JS values are returned back from Luau safely and exact, with no modifications
  • ✓ Luau calls no longer have stack pollution in a rare case where a stack pop didn’t occur
  • ✓ Luau now supports JS class instances, which adds compatibility for a lot of things
  • ✓ Directly dropping in and using any node module in Luau should work out of the box
  • ✓ Merged luau (@690) to luau-interop

This makes the following now possible:

import { LuauState, InternalLuauWasmModule } from "luau-web"
import Express from "express";

InternalLuauWasmModule.onRuntimeInitialized = () => {
	const state = new LuauState({
		Express
	});

	state.loadstring(`
local app = Express()

app.get('/', function(req, res)
	res.send('hello from Luau Web')
end)

app.listen(3000)`, "index.ts", true)();
};
1 Like

Genuinely the best community resource I have ever laid my eyes upon

2 Likes

I know that this says that wait cannot be made but is there at least some way to replicate it?

Edit: Is there also a way to add a custom file system so I could use require?

Thanks :pray:

you can make wait, but it will only work synchronously. if you don’t understand what i mean then it’s okay but Roblox’s Luau runtime has actual support for multithreading and coroutines are more like threads then coroutines are in base Luau (you can’t run multiple coroutines at once).

this is the worst implementation of wait:

const state = new LuauState({
	wait: function (n: number) {
		const buf = new SharedArrayBuffer(4)
		const int32 = new Int32Array(buf)
		Atomics.wait(int32, 0, 0, n * 1000)
	}
});

this is STILL very bad and very unoptimized especially for larger values even though it’s not a busy loop with Date() and checking if the time has passed, but in the future you will be able to make a smoother wait (still not multithreaded) but not unstable by making your function async and awaiting a promise which will be supported in the next update

what it will look like in the future
const state = new LuauState({
	wait: async function (n: number) {
		await new Promise((res) => { setTimeout(res, n * 1000) })
	}
});

you don’t need to do all that, you can simply just call loadstring on files you read based on the path and it’ll return the results of the call, which will run with safeenv and other stuff which opens up potential security issues but it probably will be ok. i’ll write some example code that works but you should be able to do this already without me showing you

examples
-- lib.lua
return {
    func1 = function()
        print("func1")
        return 5
    end
}
import { LuauState, InternalLuauWasmModule } from "luau-web"
import * as path from "path";
import * as fs from "fs";

InternalLuauWasmModule.onRuntimeInitialized = () => {
	const state = new LuauState({
		require: function (p: string) {
			const cwd = process.cwd();
			const full = path.resolve(cwd, p);

			if (!full.startsWith(cwd)) {
				throw new Error("require outside cwd not allowed");
			}

			if (!/\.(lua|luau)$/.test(full)) {
				throw new Error("only .lua or .luau files allowed");
			}

			return state.loadstring(fs.readFileSync(full, "utf8"), full, true)();
		}
	});

	state.loadstring(`
local lib = require('./lib.lua')
print(lib.func1())`, "index.ts", true)(); // prints 5
};

my codebase of the fork is so stable that this worked first try which is nice

1 Like

This is so cool. Support for JS classes is huge.

1 Like

For some reason this is not doing anything

import { LuauState, InternalLuauWasmModule } from "/modules/luau/index.js";

InternalLuauWasmModule.onRuntimeInitialized = () => {
	const a = new LuauState;
	const func = a.loadstring("print('hi')", "A");
	func()
}

It looks a little different than usual because I am doing this in a Chrome extension

Also is there a way to access low level C functions like lua_newstate or no?

Sorry for the inconvenience
Thanks :pray:

NVM Got working somehow Let’s gooo

BTW this is probably better because it won’t work if createAsync is called AFTER it has been loaded

static async createAsync(env) {
	if (!Luau["calledRun"]) { // before: !Luau.onRuntimeInitialized
		await new Promise(resolve => {
			Luau.onRuntimeInitialized = resolve;
		});
	}

	const instance = new LuauState(env);
	return instance;
}

And can’t you make the Luau environment automatically async so it doesn’t freeze the page if a single Luau script freezes and then You can implement await by automatically awaiting every javascript function called in Luau with a setting (Because calling :await() to wait is weird)

Bro

Edit: Other screenshot probably happened because I was returning an array

this is a issue i found out about recently yeah it’ll be fixed in the next version 1.2

i actually experimented with asynchronous EM_JS and it did allow for async await’s so in the future it will do what you want i guess? idk it’s positive regardless because yes

lua_newstate is called internally by the library for every state you make by instantiating the class there is currently a bug that breaks when you have multiple lua states anyway and not all functions are exposed in the WASM api. it was never intended for that, all that causes is confusion, difficulty, and more security issues. maybe it could be a little useful now and then but i don’t think it’s worth adding because of it’s little to no use case

this is due to a feature that doesn’t exist yet; a feature i’ll add which is mutable JS objects, this is something didn’t anticipate because of security i froze all js tables and didn’t allow luau to modify them, but in 1.2 maybe i’ll add support for mutable tables

it’s host userdata with __type as “table” which is why the error says table expected got table, i kind of don’t want to allow it to be detected in that way which is why i renamed it to be called that

1 Like

Praying for version 1.2 :pray: :pensive:

Btw is there a way to add custom types like Roblox luau does

Eg typeof Instance is “Instance”

Yeah maybe every JavaScript function call in luau should be awaited automatically because the user can just task.spawn if they want anyways instead of complicating things and adding a custom await and async feature in luau

1 Like

that’s a good idea. i was already planning for this by allowing you to retrieve globals from the already existing env

this would mean you could also change it at runtime (+positive)
it would mean you could remove stuff (+positive)
and you would also be able to use stuff like buffer.readu8(buffer, 0) (+positive)

another benefit is you wont need to just always have a constant global object that you can’t changekinda not true, because you can change subdescendant tables but that’s beside the point

after i do that, i would also want to add some extra things that aren’t globals in Luau already, but only for the JS side of things

like their own version of exploit functions, but only for utility like:
getrawmetatable(T) (get a Luau/JS table’s metatable regardless of __metatable field)
setreadonly(T, boolean) (freeze or unfreeze a Luau/JS table)

this is going to be good because it will allow a lot of things like being able to call __index on userdata coming from the Luau runtime which is something you aren’t able to do currently, but you will

I think this will come in 1.3 instead of 1.2 because I want to take time to make things stable an secure, but there are still useful features coming in 1.2

1 Like

Maybe to make this more streamlined you could add luauState.globals or luauState.environment which would be a Proxy (probably) and when you get something from it
e.g.

luauState.globals.print

Would return the print function in Luau

And you can set your own like this

luauState.globals.foo = 1

Which would expose the foo variable to the Luau environment

Instead of doing

new LuauState({ ... })

&

luauState.setEnvironment(...)
1 Like

oh yeah. i forgot to mention

because i’m adding JS metatable support, and it is going to be host metatable you will be able to set __type in your own metatables to a string.

errors, typeof, and things like that will show that type (that’s why JS objects show “table” but aren’t really tables, and JS functions show “function” because they’re both host created userdata)

newproxy() which creates a userdata for the user is tagged with UTAG_PROXY and lua will not care about the __type field for security

i might keep the former just for compatibiliy and people might not need to change at runtime so it feels better to just do an object once, and so that you could always mix both together which is probably what i’d do,

but i agree with your design choice i think it is the best. thank you

1 Like

For some reason when I set an already existing global (e.g. vector) to null or undefined it will still print the original global

Code example:

await LuauState.createAsync({ vector: undefined });
// or
await LuauState.createAsync({ vector: null });

Result:

loadstring("print(vector)")()
Luau.Web.js:1 table: 0x0000000000122148
[]
1 Like

hm. even though this is undefined behavior this SHOULD still work technically, because it would push a nil value then set the global of that name.

the reason i know this works is because you can modify them to an actual value, but i guess without a value it just ignores it? i agree this is an issue though and i’ll make one to track it, will be in 1.2

1 Like

BTW How would new Class be implemented in Luau? Would it automatically have a method like new so it can be created with Class.new() or do the Classes have to manually add a static .new function?

mm, i was more or so thinking of the class itself being called as a function, and it would return what it would be like with new

the reason i think this is the best is because it doesn’t clash with any logic, in JS you can’t call a class like a function, so it’s the best option in my opinion to make it in lua if you call a class it’ll be the same thing as instantiating it.

2 Likes