Why our JavaScript ↔ Lua interface?Pourquoi notre interface JavaScript ↔ Lua ?
We want Lua to handle an application’s data and call the operations it entrusts to it. The host keeps control of its objects and execution.Nous voulons que Lua manipule les données d’une application et appelle les opérations que celle-ci lui confie. L’hôte garde la maîtrise de ses objets et de l’exécution.
PyroeisVm therefore provides an exchange interface built into the engine. This choice serves game rules, strategies and scripts embedded in an application. It explains why we do not directly use fengari-interop.PyroeisVm fournit donc une interface d’échange intégrée au moteur. Ce choix répond aux besoins des règles de jeu, des stratégies et des scripts intégrés dans une application. Il explique pourquoi nous n’utilisons pas directement fengari-interop.
Try the exchanges both waysEssayer les échanges dans les deux sens: data, JavaScript callbacks, multiple returns and references to Lua functions. : données, callbacks JavaScript, retours multiples et références à des fonctions Lua.
Copied data and explicit operationsDes données copiées et des opérations explicites
A plain JavaScript object passed to Lua becomes a table built from its own enumerable properties. An array becomes a table indexed from 1. Lua can modify these tables without automatically modifying the original JavaScript objects. Their prototypes are not exposed by this conversion.Un objet JavaScript ordinaire transmis à Lua devient une table construite à partir de ses propriétés propres énumérables. Un tableau devient une table indexée à partir de 1. Lua peut modifier ces tables sans modifier automatiquement les objets JavaScript d’origine. Leurs prototypes ne sont pas exposés par cette conversion.
The other way round, Lua tables become JavaScript arrays, objects or Maps depending on their keys. They are snapshots of their data: their metatables and behaviour do not travel along. Calls use run, call and functions registered with defineFunction.Dans l’autre sens, les tables Lua deviennent des tableaux, des objets ou des Map JavaScript suivant leurs clés. Ce sont des instantanés de leurs données : leurs métatables et leur comportement ne sont pas transportés. Les appels utilisent run, call et les fonctions enregistrées avec defineFunction.
const { PyroeisVm } = require('@datamoc/pyroeis');
const vm = new PyroeisVm(undefined, undefined, {
sandbox: true,
limits: { instructions: 30000, timeMs: 50 }
});
vm.run(`
function choose(targets)
local best = 1
for i, target in ipairs(targets) do
if target.distance < targets[best].distance then best = i end
end
targets[best].distance = 0 -- only modifies the Lua tablemodifie seulement la table Lua
return best
end
`);
const targets = [{ distance: 8 }, { distance: 3 }];
const selected = vm.call('choose', targets); // 2 (Lua index)
console.log(targets[1].distance); // 3, unchanged JS objectobjet JS inchangé
if (!Number.isInteger(selected) || selected < 1 || selected > targets.length) {
throw new Error('Invalid target index');
}
const target = targets[selected - 1]; // the host validates and appliesl’hôte valide et applique
This is the model of the MWG demo: the game engine keeps its map, pathfinder and rendering objects. It sends target descriptions to the Lua worker and receives a validated index. The script gets no game-engine object by reference.C’est le modèle de la démo MWG : le moteur de jeu conserve sa carte, son pathfinder et ses objets de rendu. Il envoie des descriptions de cibles au worker Lua et reçoit un indice validé. Le script ne reçoit aucun objet du moteur de jeu par référence.
When a script must act on the host, we expose one precise operation:Lorsqu’un script doit agir sur l’hôte, nous exposons une opération précise :
vm.defineFunction('game.log', message => {
if (typeof message !== 'string' || message.length > 200) {
throw new Error('Expected a message of at most 200 characters');
}
console.log(message);
});
vm.run('game.log("Ready")');
This callback provides a single operation with argument validation. It does not automatically expose console, the DOM or the rest of the application’s objects. A callback returning an object that holds other functions does widen the interface, though: passed JavaScript functions become callable host functions.Ce callback fournit une seule opération avec validation des arguments. Il n’expose pas automatiquement console, le DOM ou le reste des objets de l’application. Un callback qui renvoie un objet contenant d’autres fonctions élargit toutefois l’interface : les fonctions JavaScript transmises sont converties en fonctions hôtes appelables.
What fengari-interop offersCe que propose fengari-interop
The upstream library keeps JavaScript objects as userdata and lets you manipulate Lua values from JavaScript through wrappers. Its js library notably gives access to js.global, object construction and proxies. It suits trusted Lua working directly with JavaScript APIs. These behaviours are described in its README and its implementation.La bibliothèque amont conserve les objets JavaScript sous forme de userdata et permet de manipuler des valeurs Lua depuis JavaScript via des wrappers. Sa bibliothèque js donne notamment accès à js.global, à la construction d’objets et aux proxies. Elle convient à du Lua de confiance qui travaille directement avec des API JavaScript. Ces comportements sont décrits dans son README et son implémentation.
| NeedBesoin | Pyroeis choiceChoix Pyroeis | fengari-interop modelModèle fengari-interop |
|---|---|---|
| JavaScript objectsObjets JavaScript | Data conversion; no implicit synchronisation.Conversion des données ; aucune synchronisation implicite. | References kept, with object deduplication.Références conservées, avec déduplication des objets. |
| Lua tables received by JavaScriptTables Lua reçues par JavaScript | Snapshots fitted to the application.Instantanés adaptés à l’application. | Wrappers giving access to the Lua value.Wrappers donnant accès à la valeur Lua. |
| Host operationsOpérations de l’hôte | Functions chosen and registered explicitly.Fonctions choisies et enregistrées explicitement. | General bridge to the exposed JavaScript objects.Pont général vers les objets JavaScript exposés. |
| Execution controlContrôle d’exécution | Calls, references and coroutines go through the VM API and its budgets.Appels, références et coroutines passent par l’API de la VM et ses budgets. | The library is an interoperability bridge; its functions are not an equivalent budgeting API.La bibliothèque est un pont d’interopérabilité ; ses fonctions ne constituent pas une API de budgets équivalente. |
We choose the Pyroeis interface to make the script’s inputs, results and actions explicit. This is not a claim that fengari-interop is inherently unsafe, nor that our conversion is always faster.Nous choisissons l’interface Pyroeis pour rendre les entrées, les résultats et les actions du script explicites. Cela ne constitue pas une affirmation selon laquelle fengari-interop serait intrinsèquement dangereux, ni selon laquelle notre conversion serait toujours plus rapide.
An interface tied to the VM lifecycleUne interface liée au cycle de vie de la VM
We keep one common API for protected calls, LuaError and LimitError errors, coroutines and budgets. A Lua function read from the VM becomes a LuaRef anchored in its registry; calling it goes back through the VM. Threads and full userdata also use references. They belong to one specific VM, and release() frees them explicitly. Automatic collection, where available, remains an aid with no guaranteed timing.Nous conservons une API commune pour les appels protégés, les erreurs LuaError et LimitError, les coroutines et les budgets. Une fonction Lua lue depuis la VM devient un LuaRef ancré dans son registre ; son appel repasse par la VM. Les threads et les userdata complets utilisent aussi des références. Elles appartiennent à une VM précise, et release() permet de les libérer explicitement. La collecte automatique, lorsqu’elle est disponible, reste une aide sans délai garanti.
The exchange types also express choices useful to our applications: Lua integers beyond Number’s safe precision read as BigInt; a non-UTF-8 Lua string reads as Uint8Array; LuaFloat distinguishes an integer-valued float; Multi and Yield represent multiple returns and suspension. Userdata allows explicit opaque passing, with no general proxy added automatically.Les types d’échange expriment aussi des choix utiles à nos applications : les entiers Lua dépassant la précision sûre de Number sont lus comme BigInt ; une chaîne Lua non UTF-8 est lue comme Uint8Array ; LuaFloat distingue un flottant entier en valeur ; Multi et Yield représentent des retours multiples et une suspension. Userdata permet un passage opaque explicite, sans proxy général ajouté automatiquement.
Waiting for a JavaScript answer before continuing LuaAttendre une réponse JavaScript avant de poursuivre Lua
A user dialogue, a load or another asynchronous task can suspend Lua without blocking the page. A host callback returns new Yield([request]) to a coroutine. Its resume() then returns { done: false, values: [request] }, and the host takes over the request. After awaiting its result in JavaScript, it calls resume(result): that result becomes the callback’s return value in Lua, which continues where it left off.Un dialogue utilisateur, un chargement ou un autre traitement asynchrone peut suspendre Lua sans bloquer la page. Un callback hôte renvoie new Yield([request]) à une coroutine. Son resume() retourne alors { done: false, values: [request] }, et l’hôte prend en charge la demande. Après avoir attendu son résultat en JavaScript, il appelle resume(result) : ce résultat devient la valeur de retour du callback dans Lua, qui poursuit au même endroit.
The dialogue demoLa démonstration de dialogue asks two successive choices and keeps the first in a Lua variable during the second. Waiting for the user is not active Lua execution; each resumption gets the VM budget. This mechanism is explicit: callbacks do not automatically make a JavaScript Promise awaitable by Lua. The host must organise suspension, answer and cancellation.demande deux choix successifs et conserve le premier dans une variable Lua pendant le second. L’attente de l’utilisateur ne constitue pas de l’exécution Lua active ; chaque reprise reçoit le budget de la VM. Ce mécanisme est explicite : les callbacks ne rendent pas automatiquement une Promise JavaScript attendable par Lua. L’hôte doit organiser la suspension, la réponse et l’annulation.
The costs and limits of this choiceLes coûts et les limites de ce choix
- Copies take time and memory. Large structures, frequent calls and objects whose identity must be shared can justify another adapter.Les copies prennent du temps et de la mémoire. Les grandes structures, les appels fréquents et les objets dont l’identité doit être partagée peuvent justifier un autre adaptateur.
- JavaScript classes, their prototype methods and Lua metatables are not reproduced by a data conversion. An API must be designed for the behaviours needed.Les classes JavaScript, leurs méthodes de prototype et les métatables Lua ne sont pas reproduites par une conversion de données. Il faut concevoir une API pour les comportements nécessaires.
- Reading a Lua table preserves its cycles and tables shared within one conversion. Converting a JavaScript graph to Lua does not memoise its identity: shared references are recopied and cycles end up refused by the depth limit.La lecture d’une table Lua préserve ses cycles et les tables partagées au sein d’une conversion. La conversion d’un graphe JavaScript vers Lua ne mémorise pas son identité : les références partagées sont recopiées et les cycles finissent par être refusés par la limite de profondeur.
nullandundefinedboth becomenil. A table entry assignednildisappears; so not all JavaScript data round-trips unchanged.nulletundefineddeviennent tous deuxnil. Une entrée de table à laquelle on affectenildisparaît ; toutes les données JavaScript ne font donc pas un aller-retour sans changement.- A JavaScript callback remains trusted host code. A Lua budget cannot preempt an arbitrary JavaScript loop, undo its external effects or cap all its allocations. Conversions themselves can walk a lot of data; the host must bound inputs and outputs.Un callback JavaScript reste du code hôte de confiance. Un budget Lua ne peut pas préempter une boucle JavaScript arbitraire, annuler ses effets externes ni plafonner toutes ses allocations. Les conversions elles-mêmes peuvent parcourir beaucoup de données ; l’hôte doit borner les entrées et les sorties.
sandbox: truedoes not remove the functions you choose to expose. For untrusted code, combine a restricted interface, budgets, result validation and suitable outer isolation, like the demo’s worker.sandbox: truene retire pas les fonctions que vous choisissez d’exposer. Pour du code non fiable, associez une interface restreinte, des budgets, la validation des résultats et une isolation extérieure adaptée, comme le worker de la démo.
Why not simply plug in the upstream library?Pourquoi ne pas brancher simplement la bibliothèque amont ?
Version 0.1.4 under review imports fengari and declares that package as peer dependency ^0.1.0 in its manifest. Pyroeis is another package and continues its own Lua implementation. Renaming the import would not prove compatibility of states and of the C API.La version 0.1.4 examinée importe fengari et déclare ce paquet comme dépendance pair ^0.1.0 dans son manifest. Pyroeis est un autre paquet et poursuit sa propre implémentation Lua. Changer le nom de l’import ne prouverait pas la compatibilité des états et de l’API C.
A port would need to verify that compatibility, the call paths, reference handling, numeric conversions and the accesses granted to the script. We do not provide that adapter today as a supported integration. For our current uses, the PyroeisVm interface directly provides the contract we want to maintain and test.Un portage devrait vérifier cette compatibilité, les chemins d’appel, la gestion des références, les conversions numériques et les accès accordés au script. Nous ne fournissons pas aujourd’hui cet adaptateur comme intégration prise en charge. Pour nos usages actuels, l’interface de PyroeisVm fournit directement le contrat que nous voulons maintenir et tester.
Comparison verified on 12 October 2026 on upstream commit e1eec1af192898cd6786bcd108e22feb40b097e3. Pyroeis contract: implementation and types.Comparaison vérifiée le 12 octobre 2026 sur le commit amont e1eec1af192898cd6786bcd108e22feb40b097e3. Contrat Pyroeis : implémentation et types.