Retour au cours

games / lua

Manipulation binaire : bit ops et string.pack/unpack

Leçon 161 exercice

Explication

Ce que vous allez apprendre

  • Manipuler des bits individuels avec les opérateurs &, |, ~, <<, >> (Lua 5.3+)
  • Stocker plusieurs indicateurs booléens dans un seul nombre entier (bitflags)
  • Sérialiser des valeurs typées en octets avec string.pack/string.unpack
  • Fixer explicitement l'endianness (</>) pour garantir la compatibilité entre systèmes
  • Construire un mini-protocole binaire compact pour un paquet réseau custom

Dans quel contexte ?

Un serveur de jeu en réseau doit transmettre très fréquemment la position et la santé de chaque joueur à tous les autres clients connectés. Encoder ces informations en JSON textuel fonctionne, mais gaspille de la bande passante ; les encoder en binaire compact avec string.pack réduit chaque paquet à quelques octets précis, un gain non négligeable multiplié par le nombre de joueurs et la fréquence d'envoi.

Code de formatType C équivalentTaille
I4uint32_t4 octets
I2uint16_t2 octets
Buint8_t (byte)1 octet
ffloat4 octets

Piège fréquent

Omettre l'indicateur d'endianness (< pour little-endian, > pour big-endian) dans le format de string.pack fonctionne tant que l'encodage et le décodage se font sur la même machine, mais casse silencieusement dès qu'un paquet binaire est partagé entre systèmes différents (client/serveur sur des architectures distinctes).

Deux outils pour descendre au niveau des octets

Jusqu'ici, ce cours a manipulé des valeurs Lua ordinaires (nombres, chaînes, tables). Cette leçon aborde un niveau plus bas : manipuler directement des bits individuels et construire des séquences d'octets précises — indispensable pour communiquer avec un protocole réseau, lire un format de fichier binaire, ou encoder efficacement des données pour les transmettre.

Les opérateurs bit à bit : manipuler des indicateurs individuels

Un nombre entier peut servir à stocker plusieurs informations booléennes indépendantes en même temps, chacune représentée par un bit distinct — une technique classique pour économiser de la mémoire (un ensemble de droits d'accès, par exemple, tient dans un seul nombre plutôt que plusieurs variables booléennes séparées). & (ET) teste si un bit précis est actif, | (OU) active un bit, ~ combiné à & désactive un bit précis, et <</>> décalent tous les bits d'une position — un décalage à gauche équivaut à multiplier par une puissance de deux.

string.pack/unpack : le pont entre valeurs Lua et octets bruts

Ces deux fonctions traduisent entre des valeurs Lua typées (un entier sur 32 bits, un flottant, un octet unique) et leur représentation binaire compacte — l'inverse exact l'une de l'autre. Le format qu'on leur donne (comme "<I4 I2 B") décrit précisément la structure attendue : quel type, quelle taille, dans quel ordre.

L'endianness, un piège classique à connaître

Un même nombre peut être stocké octet par octet dans deux ordres différents selon le système (little-endian ou big-endian). Fixer explicitement cet ordre dans le format (< ou >) garantit que des données encodées sur une machine se décodent correctement sur une autre — un détail invisible tant qu'on travaille sur une seule machine, mais qui casse tout dès qu'un protocole réseau ou un fichier binaire est partagé entre systèmes différents.

Un cas d'usage concret : construire son propre protocole réseau

Encoder un paquet réseau custom (identifiant, position, santé d'un joueur) avec string.pack produit une représentation minimale en octets, bien plus compacte qu'une sérialisation textuelle comme JSON — un choix fréquent dans les jeux en réseau où chaque octet transmis compte.

Commandes & code

Manipulation binaire : bit ops et string.pack/unpack

Lua 5.3+ intègre des opérateurs bit à bit natifs et string.pack/string.unpack pour sérialiser des données binaires sans dépendance externe.

lua
-- Opérateurs bit à bit natifs (Lua 5.3+) : &, |, ~ (xor ou not unaire), <<, >>
local flags = 0
local READ, WRITE, EXEC = 1, 2, 4   -- puissances de 2 : chaque bit représente un droit distinct

flags = flags | READ | WRITE        -- active les bits READ et WRITE : 0b011 = 3
print(flags & READ ~= 0)            -- true  : le bit READ est-il actif ?
print(flags & EXEC ~= 0)            -- false : le bit EXEC n'est pas actif

flags = flags & ~WRITE              -- ~WRITE = NOT bit à bit : désactive spécifiquement le bit WRITE
print(flags)                        -- 1 (READ uniquement)

print(1 << 4)                       -- 16  : décalage à gauche, équivalent à 1 * 2^4
print(256 >> 4)                     -- 16  : décalage à droite, équivalent à 256 / 2^4 (division entière)
lua
-- string.pack : sérialise des valeurs typées en une chaîne d'octets binaire (format proche de struct.pack en Python)
local packed = string.pack(
    "<I4 I2 B",     -- < = little-endian, I4 = uint32, I2 = uint16, B = uint8 (byte)
    0xDEADBEEF,     -- magic number (4 octets)
    42,             -- version (2 octets)
    3               -- flags (1 octet)
)
print(#packed)      -- 7 octets exactement, aucun overhead

-- string.unpack : opération inverse, retourne les valeurs ET la position suivante dans la chaîne
local magic, version, flags, nextPos = string.unpack("<I4 I2 B", packed)
print(magic, version, flags)   -- 3735928559  42  3   (0xDEADBEEF en décimal)
lua
-- Cas pratique : construire un mini-protocole binaire pour un paquet réseau custom
local function encodePacket(playerId, x, y, health)
    return string.pack("<I4 f f B", playerId, x, y, health)
    -- I4 = uint32 (id joueur), f f = deux float (position), B = uint8 (vie, 0-255)
end

local function decodePacket(rawBytes)
    local id, x, y, health = string.unpack("<I4 f f B", rawBytes)
    return { playerId = id, x = x, y = y, health = health }
end

local packet = encodePacket(1001, 152.4, -87.9, 100)
print(#packet)                          -- 13 octets : 4+4+4+1
local decoded = decodePacket(packet)
print(decoded.playerId, decoded.health) -- 1001  100
lua
-- Checksum simple par XOR (détection d'erreur basique, pas cryptographique) : illustre les bit ops en pratique
local function xorChecksum(bytes)
    local checksum = 0
    for i = 1, #bytes do
        checksum = checksum ~ string.byte(bytes, i)   -- ~ en binaire (deux opérandes) = XOR bit à bit
    end
    return checksum
end

local data = "packet_payload"
local checksum = xorChecksum(data)
local withChecksum = data .. string.pack("B", checksum)

-- Côté réception : recalculer et comparer
local received = withChecksum:sub(1, -2)
local receivedChecksum = string.unpack("B", withChecksum:sub(-1))
assert(xorChecksum(received) == receivedChecksum, "Paquet corrompu détecté")

Résumé

  • Les opérateurs &, |, ~, <<, >> (Lua 5.3+) manipulent des bits individuels sans bibliothèque externe.
  • string.pack/string.unpack sérialisent des valeurs typées en binaire compact, avec un format proche de struct en C/Python.
  • Toujours fixer l'endianness explicitement (< ou >) dans le format pour garantir la compatibilité inter-plateformes.
  • Un XOR checksum illustre les bit ops mais ne remplace jamais un vrai hash cryptographique (voir leçon cryptographie).

Exercices pratiques

1 disponible
1

Mission : les paquets réseau corrompus entre serveur Linux et client Windows

Objectif : Diagnostiquer un problème d'endianness dans un protocole binaire custom et corriger un système de bitflags de permissions.

Contexte

Un serveur de jeu encode la position des joueurs pour l'envoyer aux clients :

lua
local function encodePacket(playerId, x, y, health)
  return string.pack("I4 f f B", playerId, x, y, health)   -- pas d'indicateur d'endianness !
end

Le serveur tourne sur une machine Linux, et un client sur une architecture différente décode des positions complètement fausses (des joueurs qui apparaissent à des milliers de kilomètres de leur vraie position), alors que le même code fonctionne parfaitement en test sur une seule machine. Par ailleurs, le système de permissions doit gérer trois droits (LIRE=1, ECRIRE=2, EXECUTER=4) combinés dans un seul entier.

Résoudre l’exercice →