Interop .NET
Le namespace YScript.Dotnet fait le pont entre le monde C#/.NET et les valeurs manipulées par un script :
exposer un objet/type .NET existant à un script (sans réécrire de bibliothèque de fonctions à la
main), et à l'inverse laisser un script explorer le monde .NET par réflexion via la bibliothèque
optionnelle dotnet.
Ce document couvre le point de vue de l'hôte (comment exposer du C#). Le comportement scriptable de
dotnet.* une fois ouverte est hors périmètre ici (bibliothèque standard, pas API du moteur) — seul
le principe général est rappelé en fin de page.
Pousser un objet .NET existant
PushObject pousse n'importe quel objet C# sur la pile ; les types primitifs (bool, entiers,
flottants, string, enum) sont convertis en valeur script native, tout le reste est poussé comme
userdata avec une métatable qui pilote son comportement vu du script (indexation, appel,
opérateurs...) :
engine.PushObject(myOrder); // pousse un userdata pour l'instance
engine.SetGlobal("order");
print(order.Total) -- accès à une propriété .NET via __index
order:MarkAsPaid() -- appel d'une méthode .NET personnalisée (voir plus bas pourquoi ':')
Par défaut (sans métatable enregistrée pour le type), l'accès aux membres passe par réflexion pure
sur le type de l'objet (DynamicMetatable, décrite plus bas). Pour un type utilisé fréquemment ou
qui a besoin d'un comportement sur mesure, enregistrez-le explicitement une fois au démarrage plutôt
que de laisser la résolution par réflexion se faire à chaque nouvel accès :
engine.RegisterType<Order>();
Point ou deux-points pour appeler une méthode .NET ?
obj:methode(...) est le sucre syntaxique du langage (voir functions.md
§Méthodes et self) pour obj.methode(obj, ...) — l'objet est repassé en premier argument. Que ce
soit la bonne syntaxe dépend entièrement de comment la méthode a été exposée côté .NET :
- Membre découvert automatiquement par réflexion (le cas courant : une méthode d'instance
ordinaire sur le type, sans attribut particulier) —
Index(DynamicMetatable) la retourne déjà liée à l'instance (DotnetMethod.Sourcecapturé au moment de l'accèsobj.methode, avant même l'appel — comparable à un bound method Python). Un appel en deux-points repasse alors l'objet une deuxième fois, en premier argument.DotnetHelpers.InvokeObjectdétecte ce cas (le premier argument est littéralement le même objet queSource) et ignore ce premier argument s'il existe une surcharge qui consomme alors tous les arguments restants —obj:Methode(x)etobj.Methode(x)sont donc équivalents dans l'immense majorité des cas. Limite connue et acceptée (heuristique, pas une distinction garantie) : un appel qui repasse délibérément l'objet lui-même comme véritable premier argument (obj.Comparer(obj), ex. unEquals-like) est indiscernable, à la seule valeur des arguments, d'un deux-points accidentel — s'il existe une surcharge compatible qui matche intégralement une fois cet argument retiré (typiquement une surcharge à zéro paramètre), c'est cette lecture qui l'emporte, silencieusement. VoirDotnetHelpersTest, testsSelfSugar_*(dontSelfSugar_KnownLimitation_*) pour le détail exact du compromis. En cas de doute persistant (une méthode censée recevoir l'objet lui-même comme argument se comporte bizarrement), le point reste la syntaxe sans surprise. - Fonction personnalisée
[MetaFunction]/[MetaEvent]sur la métatable (commeMarkAsPaidci-dessus) — c'est une fonction de style C Lua ordinaire, qui lit explicitementselfà l'indice de pile 1 dans son propre code (script.CheckType(1, ...).ToUserData(1)) ; rien n'est pré-lié. → deux-points obligatoire (ou repasser l'objet en premier argument explicite avec le point), sans quoi l'indice 1 est vide/incorrect — ce cas n'est pas concerné par le mécanisme ci-dessus (il ne passe pas parDotnetMethod).
Retour multivalue via Tuple/ValueTuple
Une méthode .NET dont le type de retour est un tuple (ValueTuple<...>, y compris la syntaxe C#
(string a, int b), ou l'ancien Tuple<...>) ne pousse pas un unique userdata représentant la
tuple : DynamicMetatable.DefaultMetaCall détecte ce cas (DotnetHelpers.IsTupleType) et pousse
chaque élément séparément (DotnetHelpers.FlattenTuple), exactement comme un return a, b en
script :
public (string Name, int Total) GetSummary() => (Name, Total);
local name, total = order.GetSummary()
Les noms des éléments (Name, Total) n'existent qu'au niveau du compilateur C# (attribut
TupleElementNamesAttribute sur la méthode) — ils ne sont pas accessibles côté script, seul l'ordre
des valeurs compte. Toutes les arités (1 à 8, y compris au-delà de 7 où ValueTuple imbrique le
surplus dans un élément Rest) sont aplaties récursivement en autant de valeurs de retour.
Ce mécanisme ne s'applique qu'à l'appel __call par défaut (obj.Methode(...), résolution par
réflexion ou délégué) : une fonction [MetaFunction]/[MetaEvent] personnalisée reste responsable
elle-même du nombre de valeurs qu'elle pousse.
Métatables : MetaTableAttribute et DynamicMetatable
Une métatable .NET est une classe ordinaire dont les méthodes taguées [MetaEvent("__index")],
[MetaEvent("__call")], etc. implémentent les évènements Lua-like du type (DynamicMetatable est
l'implémentation par défaut : __index/__newindex délèguent à la réflexion sur le type .NET lié,
__call invoque l'objet s'il est invocable).
[MetaTable] sur le type .NET lui-même indique quelle classe utiliser comme métatable :
[MetaTable(typeof(OrderMetatable))]
public class Order { public decimal Total { get; set; } /* ... */ }
public class OrderMetatable : DynamicMetatable
{
[MetaFunction("markpaid")]
public int MarkAsPaid(IScript script)
{
var order = (Order)script.CheckType(1, ScriptValueType.UserData).ToUserData(1);
order.MarkAsPaid();
return 0;
}
}
[MetaFunction("nom")]ajoute une fonction supplémentaire à la métatable, en plus des membres découverts automatiquement par réflexion sur le type lié — utile pour un nom scriptable différent du nom .NET, ou une opération qui n'existe pas comme méthode .NET ordinaire.[MetaProperty("nom", MetaPropertyKind.Get\|Set)]ajoute un accesseur de propriété personnalisé, avec la même signatureint Method(IScript script)qu'une fonction : l'objet lié est à l'indice de pile 1, et pour un setter la nouvelle valeur est à l'indice 3 (2 étant le nom de la propriété).[MetaEvent("__xxx")](sur une méthode de la métatable) l'associe à un évènement précis (__index,__newindex,__call, ...) — c'est ce queDynamicMetatableutilise en interne pourIndex/NewIndex/Call.[MetaAsType(typeof(Base))](sur le type .NET) force l'enregistrement/la résolution de sa métatable sous un type différent du type réel de l'objet — utile pour une hiérarchie de types où toutes les sous-classes doivent partager la métatable de la classe de base.
RegisterType<T>()/RegisterType<TObj, TMeta>()/RegisterMetatable<T>(name) enregistrent
explicitement une association type ↔ métatable avant le premier usage (PushObject le fait aussi
paresseusement au premier objet rencontré d'un type donné s'il n'a pas déjà été enregistré).
Convertir une valeur de script vers .NET
Dans l'autre sens, IScriptValue.ToObject<T>(...)/ToObject(Type, ...) convertit une valeur du
script vers un type .NET cible (utilisé en interne pour convertir les arguments d'une méthode .NET
appelée depuis un script) :
double a = script.Get(1).ToObject(0.0, engine.Culture);
La bibliothèque dotnet côté script
OpenDotnetLib() (voir démarrage.md) publie une bibliothèque dotnet donnant à un
script un accès réflexif au CLR : charger une assembly, résoudre/instancier un type par son nom,
convertir un objet, créer un tableau .NET typé, s'abonner à un évènement .NET... Comme évoqué dans
démarrage.md, c'est délibérément la seule bibliothèque non incluse dans OpenLibs()
et à n'ouvrir que pour des scripts de confiance : elle contourne entièrement les métatables décrites
plus haut et donne accès à n'importe quel type .NET atteignable depuis le process hôte, pas
seulement à ceux explicitement exposés via PushObject/RegisterType. Le détail de ses fonctions
scriptables (dotnet.new, dotnet.type, dotnet.using, ...) relève de la bibliothèque standard,
pas de cette section.