Modules externes
En complément d'OpenLibs()/OpenIOLib()/etc. (bibliothèques intégrées au moteur, voir
démarrage.md) et de l'exposition ad hoc d'objets .NET (voir
interop-dotnet.md), YScript a un système de modules (namespace
YScript.Modules) pour packager un ensemble de fonctions C# comme une bibliothèque scriptable
cohérente. Un module externe pousse cette idée un cran plus loin : c'est le même mécanisme, mais
packagé dans une assembly .NET séparée de YScript.dll, chargée dynamiquement à l'exécution via
require("nom") — sans que votre application hôte (console, éditeur, service embarquant le moteur)
ait besoin d'une référence de compilation vers ce module. C'est la façon d'étendre YScript avec vos
propres fonctions C# sans toucher au moteur lui-même, et de distribuer cette extension indépendamment
(y compris via ypack, voir plus bas).
Les modules externes zip, json et http, distribués avec YScript, illustrent ce mécanisme dans
la pratique — chacun a sa propre documentation utilisateur (voir §5).
1. Structure d'un projet de module
Un module est un projet netstandard2.0 classique (comme l'assembly du moteur elle-même, pour rester
consommable depuis .NET Framework et .NET récent), avec une seule référence — le moteur :
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Reference Include="YScript" HintPath="chemin\vers\YScript.dll" Private="false" />
</ItemGroup>
</Project>
La classe du module hérite de YScript.Modules.BaseScriptModule et porte l'attribut [Module], qui
définit son nom d'enregistrement (celui utilisé côté script dans require("nom")) ; chaque méthode
publique int Xxx(IScript script) taguée [Function("nom")] devient une fonction exposée dans la
table du module :
using YScript.Modules;
namespace MyCompany.Modules.Greetings
{
[Module("greetings", IsGlobal = true)]
public class GreetingsModule : BaseScriptModule
{
[Function("hello")]
public int Hello(IScript script)
{
script.PushString($"Bonjour, {script.CheckString(1)} !");
return 1;
}
}
}
local greetings = require("greetings")
print(greetings.hello("monde")) --> Bonjour, monde !
[Module("nom", IsGlobal = ...)]:nomest ce que les scripts passeront àrequire(nom).IsGlobaldétermine si le module est aussi posé dans la table globale une fois chargé (commetable,math, etc.), en plus d'être retourné parrequire.[Function("nom")]sur chaque méthode : nom de la fonction exposée dans la table du module. Sans attribut, le nom de la méthode en minuscule est utilisé.- Une bibliothèque peut définir plusieurs classes
BaseScriptModule(donc plusieurs modules) dans le même assembly ; chacune est retrouvée par son propre[Module("...")].
Rien d'autre n'est requis côté module pour l'API de manipulation de script elle-même : elle est
strictement identique à celle des bibliothèques internes du moteur (namespace YScript.Libraries) —
mêmes conventions de pile (IScript, script.CheckString, script.PushString, etc.), mêmes
extensions (ScriptAuxiliary, ScriptExtensions). Un module externe n'est donc pas un mécanisme à
part : c'est la même bibliothèque scriptable que celles fournies par le moteur, seulement compilée et
distribuée en dehors de YScript.dll.
Messages et localisation
Comme pour le cœur du moteur, tout message destiné à l'utilisateur ou au script
(script.Error(...), script.TypeError(...), avertissements) doit venir d'un dictionnaire de
ressources .resx/SR*, jamais d'une chaîne littérale inline en C# : un dossier Locales/ à la
racine du projet du module, un ou plusieurs .resx associés à une classe générée (ResXFileCodeGenerator),
des clés nommées Err_XxxYyy dont les valeurs sont en français, la classe SR générée gardée
internal. Dans le code, SR.Err_XxxYyy (avec paramètres de formatage) plutôt qu'une chaîne en dur.
2. Chargement dynamique côté script (require)
Le chargement d'un module externe passe entièrement par la bibliothèque package déjà présente dans
le moteur — un module externe n'a besoin d'aucun mécanisme propre. require("json") par exemple :
- Recherche du fichier
.dllviapackage.dotnetpath(configurable par la variable d'environnementYSCRIPT_DOTNET_PATH), qui suit le même format quepackage.path: entrées séparées par;,?remplacé par le nom passé àrequire(...). - Chargement de l'assembly trouvée.
- Résolution du type module : parmi les types publics non abstraits de l'assembly dérivant de
BaseScriptModule, celui dont le nom résolu (attribut[Module("...")]) correspond exactement au nom demandé. - Instanciation et enregistrement : le type est instancié et
OpenModule(script)est appelé — exactement comme le feraitscript.Require<T>()pour un module statiquement référencé.
⚠️ Piège principal : la résolution du fichier .dll et la résolution du type module à
l'intérieur sont deux étapes indépendantes. Le type est retrouvé par réflexion via [Module("nom")]
(le nom de la classe C# n'a aucune importance) ; mais le fichier .dll lui-même doit être
physiquement trouvé par substitution littérale du ? dans le gabarit package.dotnetpath, avec la
casse exacte de l'argument passé à require(...). Recommandation : nommez l'assembly compilé du
module (donc le nom du projet .csproj) exactement comme le nom du module utilisé dans
require(...), en minuscules (ex. projet json.csproj → json.dll, gabarit ?.dll par défaut,
fonctionne sans configuration supplémentaire sur toutes les plateformes) — Windows/NTFS tolère une
casse différente entre le fichier et l'argument de require, mais Linux/macOS non.
3. Chargement côté hôte
Deux façons d'utiliser un module dans une application hôte :
- Référencé statiquement (le module est une dépendance de compilation connue de votre
application) :
script.Require<MyModule>(), comme le faitOpenModuleen interne — pas besoin de passer parrequirecôté script ni parpackage.dotnetpath. - Chargé dynamiquement, sans référence de compilation (cas d'un plugin/module tiers découvert au
runtime) : c'est le scénario
require("nom")décrit ci-dessus. Pour qu'il fonctionne, l'hôte doit simplement avoir ouvert la bibliothèquepackage(incluse dansOpenLibs(), voir démarrage.md) et pointerYSCRIPT_DOTNET_PATH/package.dotnetpathvers le dossier où se trouve l'assembly du module.
Pour tester un module en développement sans le déployer, pointez YSCRIPT_DOTNET_PATH vers la
sortie de build locale plutôt que vers un dossier de déploiement, par exemple en variable
d'environnement :
D:/dev/monmodule/bin/Debug/netstandard2.0/?.dll;;
Le ;; final conserve les chemins par défaut de package.dotnetpath en plus du chemin de dev. Sous
Visual Studio (F5), la même variable se définit dans les propriétés de lancement (launchSettings.json)
du projet consommateur (l'exécutable ou l'application qui héberge le moteur). Dans les deux cas,
require("json") déclenche exactement le même pipeline package qu'en environnement de
production — aucun code de
test/chargement ad hoc n'est nécessaire.
4. Empaqueter le module avec ypack
Un module externe destiné à être publié sur la galerie a, à sa racine (à côté du .csproj), un
manifeste ypack.json :
{
"name": "mymodule",
"version": "1.0.0",
"description": "Description du module",
"author": "Votre nom",
"engines": { "yscript": ">=0.3.0" },
"main": "MyCompany.Modules.MyModule.dll",
"dependencies": {},
"provides": { "module": true, "command": false }
}
ypack pack <dossier> <dest.zip> s'exécute contre le dossier de sortie de build (typiquement
bin/Release/netstandard2.0/), pas contre la racine du projet. Tout fichier qui doit se retrouver
dans le paquet publié — ypack.json, documentation/, et tout autre fichier non compilé consommé
par le module — doit donc être copié vers ce dossier de sortie à chaque build :
<ItemGroup>
<None Update="ypack.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="documentation\**\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
Sans ypack.json copié, ypack pack traite le dossier comme un simple bundle sans manifeste plutôt
que comme un paquet publiable ; sans documentation/ copié, l'archive produite n'a pas de
documentation utilisateur alors que le dossier source du module, lui, en a une.
5. Documenter votre module pour ses utilisateurs
Cette page-ci documente comment écrire un module — pas comment en utiliser un existant. Un
module externe a sa propre documentation utilisateur (destinée à l'auteur de script qui consomme
require("votremodule")), distincte de cette doc moteur : un dossier documentation/ à la racine
du projet du module, avec au minimum un index.md (présentation, liste des fonctions exposées,
exemples d'usage, dans l'esprit du manuel de référence Lua) :
mymodule/
mymodule.csproj
MyModule.cs
ypack.json
documentation/
index.md -- point d'entrée, obligatoire
index.md suit le même plan que cette documentation-ci pour chaque fonction exposée : signature,
description, valeur de retour, comportement en cas d'erreur, exemple ```lua court quand la fonction a
une subtilité — avec, en tête de page, une table de vue d'ensemble (fonction / description) qui
résume toute l'API du module.