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 :

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;
    }
}

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.