Bibliothèque dotnet

La bibliothèque dotnet donne à un script un accès réflexif complet au monde .NET du process hôte : charger des assemblies, résoudre des types par nom, construire des instances, appeler leurs méthodes, lire/écrire leurs propriétés et champs, construire des tableaux et types génériques, s'abonner à des évènements .NET.

Avertissement — bibliothèque volontairement non chargée automatiquement

dotnet n'est pas incluse dans OpenLibs(), et ce pour des raisons de sécurité : à la différence des autres bibliothèques standard, qui sont conçues comme un bac à sable, dotnet donne à un script la même surface d'accès que du code C# tournant dans le process hôte lui-même — n'importe quel type public de n'importe quelle assembly chargée, y compris des opérations sur le système de fichiers, le réseau, les processus, ou l'introspection d'objets internes de l'application hôte. Un hôte doit l'activer explicitement, en toute connaissance de cause :

// côté hôte .NET
script.OpenLibs();
script.OpenDotnetLib();   // n'activer que pour des scripts de confiance

Si l'hôte n'a pas ouvert la bibliothèque, require("dotnet") échoue comme n'importe quel searcher qui ne trouve pas de module :

local ok, err = pcall(require, "dotnet")
if not ok then
    print("dotnet indisponible : " .. tostring(err))
end

N'activez jamais cette bibliothèque pour des scripts dont vous ne maîtrisez pas l'origine.

Vue d'ensemble

Fonction Description
dotnet.using(namespace [, ...]) Ajoute un/des espaces de noms à la liste de résolution des noms courts
dotnet.loadassembly(fileOrName) Charge une assembly .Net (fichier ou nom)
dotnet.new(typename [, ...]) Construit une nouvelle instance de typename
dotnet.type(typename) Référence vers le type typename (membres statiques, constructeur)
dotnet.generic(typename, typeArg [, ...]) Construit un type générique fermé (ex. List<int>)
dotnet.array(typename, length) Crée un tableau .Net natif de length éléments de type typename
dotnet.isinstance(value, typename) Teste si value est une instance de typename
dotnet.cast(value, typename) Rebind un objet vers un autre type de vue
dotnet.dispose(value) Appelle IDisposable.Dispose() sur value si applicable
dotnet.addevent(value, eventName, fn) Abonne la fonction fn à l'évènement eventName de value
dotnet.removeevent(value, eventName, handle) Désabonne un handler précédemment abonné
dotnet.totable(value [, depth]) Copie récursive de value en table de script

Accès aux membres d'un objet .Net

Un objet .Net renvoyé par dotnet.new/une méthode/une propriété est exposé comme un userdata avec métatable dynamique : obj.Membre lit une propriété/un champ public, obj.Membre = v l'écrit, obj:Methode(...) (ou obj.Methode(obj, ...)) appelle une méthode publique — la surcharge est résolue automatiquement selon le nombre et le type des arguments fournis. Un objet indexable (indexeur .Net, ex. this[int]) s'utilise avec obj[cle]/obj[cle] = v.

local dotnet = require("dotnet")

local sb = dotnet.new("System.Text.StringBuilder")
sb:Append("hello")
sb:Append(" world")
print(sb:ToString())     --> hello world
print(sb.Length)         --> 11

Un objet implémentant ICollection/IEnumerable/IDictionary/IEnumerator supporte #obj (longueur, ICollection) et pairs(obj) (itération — clé/valeur pour un dictionnaire, index/valeur sinon).

dotnet.using(namespace [, ...])

Ajoute un ou plusieurs espaces de noms à la liste consultée pour résoudre un nom de type court (sans namespace complet) passé à dotnet.new/dotnet.type/dotnet.generic/dotnet.array/ dotnet.isinstance/dotnet.cast. Par défaut : System, System.Text, System.Collections.Generic, System.Linq.

dotnet.using("System.IO")
local fi = dotnet.new("FileInfo", "data.txt")   -- résolu comme System.IO.FileInfo

dotnet.loadassembly(fileOrName)

Charge une assembly .Net, soit depuis un fichier (si fileOrName existe sur le disque de l'hôte), soit par nom d'assembly (déjà référencée, ou dans le GAC). Retourne true en cas de succès, ou nil plus un message d'erreur en cas d'échec.

local ok, err = dotnet.loadassembly("MyPlugin.dll")
if not ok then print(err) end

dotnet.new(typename [, ...])

Construit une nouvelle instance de typename, en résolvant automatiquement le constructeur public qui correspond le mieux aux arguments fournis. Lève une erreur si typename est introuvable, ou si aucun constructeur ne correspond aux arguments.

local list = dotnet.new("List`1[[System.Int32]]")     -- via nom fully-qualified générique
-- ou, plus lisible :
local ListInt = dotnet.generic("System.Collections.Generic.List", "System.Int32")
local list2 = ListInt()

dotnet.type(typename)

Retourne une référence vers le type typename (sans le construire) : ref.Membre lit un membre statique, ref.Membre = v l'écrit, et appeler la référence comme une fonction (ref(arg1, ...)) construit une nouvelle instance — équivalent à dotnet.new(typename, ...).

local Math = dotnet.type("System.Math")
print(Math.PI)
print(Math.Sqrt(2))     -- Sqrt est une méthode statique

local StringBuilder = dotnet.type("System.Text.StringBuilder")
local sb = StringBuilder()    -- équivalent à dotnet.new("System.Text.StringBuilder")

dotnet.generic(typename, typeArg [, ...])

Construit un type générique fermé à partir du type ouvert typename (sans le suffixe `N, ajouté automatiquement selon le nombre d'arguments de type) et d'une liste d'arguments de type (chacun un nom de type résolu comme pour dotnet.new). Retourne une référence de type, comme dotnet.type. Au moins un argument de type est requis.

local DictSI = dotnet.generic("System.Collections.Generic.Dictionary", "System.String", "System.Int32")
local d = DictSI()
d["un"] = 1

dotnet.array(typename, length)

Crée un tableau .Net natif de length éléments de type typename. L'indexation côté script est 1-based (arr[1] est le premier élément), contrairement à la convention 0-based de .Net — alignée sur les tables de script plutôt que sur .Net. length doit être positif ou nul.

local arr = dotnet.array("System.Int32", 3)
arr[1], arr[2], arr[3] = 10, 20, 30
print(#arr)      --> 3
print(arr[2])    --> 20

dotnet.isinstance(value, typename)

Retourne true si value est une instance de typename (ou d'un type qui en dérive/l'implémente).

dotnet.cast(value, typename)

Rebind value (qui doit déjà être un userdata .Net) pour être vu comme typename, sans changer l'objet .Net sous-jacent — utile pour désambiguïser une résolution de surcharge, ou pour accéder aux membres d'une interface sur un objet dont la métatable enregistrée correspond à un type moins spécifique. Lève une erreur si value n'est pas réellement une instance de typename.

dotnet.dispose(value)

Appelle Dispose() sur value s'il implémente IDisposable ; ne fait rien sinon.

dotnet.addevent(value, eventName, fn) / dotnet.removeevent(value, eventName, handle)

addevent abonne la fonction de script fn à l'évènement .Net eventName de value, en la wrappant dans un vrai délégué du type attendu par l'évènement. Retourne un handle opaque à conserver pour un désabonnement ultérieur via removeevent. Lève une erreur si eventName est introuvable sur le type de value, ou si fn n'est pas une fonction.

local timer = dotnet.new("System.Timers.Timer", 1000)
local handle = dotnet.addevent(timer, "Elapsed", function(sender, e)
    print("tic")
end)
timer.Enabled = true
-- plus tard :
dotnet.removeevent(timer, "Elapsed", handle)

dotnet.totable(value [, depth])

Copie récursivement value en une table de script ordinaire, sans lien vivant vers l'objet .Net d'origine (contrairement à l'accès direct aux membres d'un userdata, qui reflète l'état réel de l'objet) : un dictionnaire devient une table indexée par ses clés (convertibles), une liste générique devient une séquence 1..n, tout autre objet est décomposé via ses champs/propriétés publics d'instance. depth (défaut 32) borne la profondeur de récursion, contre les graphes d'objets cycliques.

local list = dotnet.generic("System.Collections.Generic.List", "System.Int32")()
list:Add(1); list:Add(2); list:Add(3)
local t = dotnet.totable(list)
print(t[1], t[2], t[3])   --> 1  2  3