動的C#クラスをメモリーリークもどきなく使用する
C#のコードをコンパイル&実行するサンプルを探していたのだけど、見つけたのがいろいろ微妙だったので、自分で書いてみた。
思想
動的に作成するクラスは、別AppDomainに作成して、解放可能にする。
実体DLLの作成は避ける。
AppDomain間通信は一つのクラスに集中させる。
(そのクラスは両方のAppDomainで持ち、MarshalByRefObjectとして自由に使う)
ついでに、値の受け渡しの仲介もする。
必要なライブラリー
これをDLLにしておく必要がある。
using System; using System.Collections.Generic; using System.CodeDom.Compiler; using System.Reflection; public class Compiler : MarshalByRefObject, IDisposable { // Compiler private CodeDomProvider _provider = null; private CompilerParameters _parameters = null; private CompilerResults _result = null; private Dictionary<string, Assembly> _assemblys = null; private Dictionary<string, object> _globals = null; private string _baseSource = @" using System; {0} public class {1} {{ public static void {2}(Junjune.Windows.Script.Compiler {3}) {{ {4} }} }} "; public Compiler() : this("CSharp") { } public Compiler(string language) { _provider = CodeDomProvider.CreateProvider(language); _parameters = new CompilerParameters(); _parameters.GenerateExecutable = false; _parameters.GenerateInMemory = true; _parameters.TreatWarningsAsErrors = false; _assemblys = new Dictionary<string, Assembly>(); _globals = new Dictionary<string, object>(); } public void Dispose() { _provider = null; _parameters = null; _assemblys.Clear(); _assemblys = null; _globals.Clear(); _globals = null; } public CompilerResults GetCompileResult() { return _result; } public static string GetUniqueName(string prefix) { return prefix + Guid.NewGuid().ToString("N"); } public string CompileSource(string asmName, string[] source) { _parameters.OutputAssembly = asmName; _result = _provider.CompileAssemblyFromSource(_parameters, source); string ret = asmName; if (_result.Errors.HasErrors) ret = null; else SetAssembly(asmName, _result.CompiledAssembly); return ret; } public string CompileSource(string asmName, string source) { return CompileSource(asmName, new string[] { source }); } public string CompileSource(string source) { return CompileSource(GetUniqueName("Assembly_"), source); } public string CompileFile(string asmName, string[] filePath) { _parameters.OutputAssembly = asmName; _result = _provider.CompileAssemblyFromFile(_parameters, filePath); string ret = asmName; if (_result.Errors.HasErrors) ret = null; else SetAssembly(asmName, _result.CompiledAssembly); return ret; } public string CompileFile(string asmName, string filePath) { return CompileFile(asmName, new string[] { filePath }); } public string CompileFile(string filePath) { return CompileFile(GetUniqueName("Assembly_"), filePath); } public CompilerParameters GetCompileParameter() { return _parameters; } public void SetReferencedAssemblies(string[] asms, bool clear) { if (clear) _parameters.ReferencedAssemblies.Clear(); if (asms != null && asms.Length > 0) { int len = asms.Length; for (int i = 0; i < len; i++) { _parameters.ReferencedAssemblies.Add(asms[i]); } } } private void SetAssembly(string asmName, Assembly asm) { lock (_assemblys) { if (_assemblys.ContainsKey(asmName)) _assemblys.Remove(asmName); _assemblys.Add(asmName, asm); } } public string[] GetAllAssemblyName() { List<string> list = new List<string>(); foreach (string key in _assemblys.Keys) list.Add(key); return list.ToArray(); } // code run public bool Run(string thisName, string codePhrase, string usingPhrase) { bool ret = false; string className = GetUniqueName("Class_"); string methodName = GetUniqueName("Method_"); string source = string.Format(_baseSource, usingPhrase, className, methodName, thisName, codePhrase); SetReferencedAssemblies(new string[] { Assembly.GetAssembly(this.GetType()).ManifestModule.Name }, false); string asmName = CompileSource(source); if (asmName != null) { string objName = Create(asmName, className); if (objName != null) { string resName = GetUniqueName("Invoke_"); if (InvokeDirect(objName, methodName, this, resName)) { ret = true; Remove(resName); } Remove(objName); } //_assemblys.Remove(asmName); } return ret; } // Reflection private object[] GetObjects(string[] paras) { object[] ps = null; if (paras != null) { int len = paras.GetLength(0); ps = new object[len]; for (int i = 0; i < len; i++) { ps[i] = Get(paras[i]); } } return ps; } private Type[] GetTypes(object[] ps) { Type[] parats = new Type[0]; if (ps != null) { int len = ps.GetLength(0); parats = new Type[len]; for (int i = 0; i < len; i++) { parats[i] = ps[i].GetType(); } } return parats; } // Constructor public string CreateDirect(string name, string asmName, string typeName, object[] paras) { string ret = null; Assembly asm = null; if (_assemblys.ContainsKey(asmName)) asm = _assemblys[asmName]; Type type = null; if (asm != null) type = asm.GetType(typeName); if (type != null) { Type[] parats = GetTypes(paras); ConstructorInfo ci = type.GetConstructor(parats); if (ci != null) { object res = ci.Invoke(paras); Set(name, res); if (res != null) ret = name; } } return ret; } public string CreateDirect(string name, string asmName, string typeName, object para) { return CreateDirect(name, asmName, typeName, new object[] { para }); } public string Create(string name, string asmName, string typeName, string[] paras) { return CreateDirect(name, asmName, typeName, GetObjects(paras)); } public string Create(string name, string asmName, string typeName, string para) { return Create(name, asmName, typeName, new string[] { para }); } public string Create(string name, string asmName, string typeName) { return CreateDirect(name, asmName, typeName, (object[])null); } public string CreateDirect(string asmName, string typeName, object[] paras) { return CreateDirect(GetUniqueName("Object_"), asmName, typeName, paras); } public string CreateDirect(string asmName, string typeName, object para) { return CreateDirect(GetUniqueName("Object_"), asmName, typeName, new object[] { para }); } public string Create(string asmName, string typeName, string[] paras) { return CreateDirect(GetUniqueName("Object_"), asmName, typeName, GetObjects(paras)); } public string Create(string asmName, string typeName) { return CreateDirect(GetUniqueName("Object_"), asmName, typeName, (object[])null); } // Method public bool InvokeDirect(string objName, string methodName, object[] paras, string resName) { bool ret = false; object tar = Get(objName); Type type = null; if (tar != null) type = tar.GetType(); if (type != null) { Type[] parats = GetTypes(paras); MethodInfo mi = type.GetMethod(methodName, parats); if (mi != null) { object res = mi.Invoke(tar, paras); if (resName != null) Set(resName, res); ret = true; } } return ret; } public bool InvokeDirect(string objName, string methodName, object para, string resName) { return InvokeDirect(objName, methodName, new object[] { para }, resName); } public bool Invoke(string objName, string methodName, string[] paras, string resName) { return InvokeDirect(objName, methodName, GetObjects(paras), resName); } public bool Invoke(string objName, string methodName, string para, string resName) { return Invoke(objName, methodName, new string[] { para }, resName); } public bool Invoke(string objName, string methodName, string resName) { return InvokeDirect(objName, methodName, (object[])null, resName); } public object InvokeDirectReturn(string objName, string methodName, object[] paras) { object ret = null; string resName = GetUniqueName("Invoke_"); if (InvokeDirect(objName, methodName, paras, resName)) { ret = Get(resName); Remove(resName); } return ret; } public object InvokeDirectReturn(string objName, string methodName, object para) { object ret = null; string resName = GetUniqueName("Invoke_"); if (InvokeDirect(objName, methodName, para, resName)) { ret = Get(resName); Remove(resName); } return ret; } public object InvokeReturn(string objName, string methodName, string[] paras) { object ret = null; string resName = GetUniqueName("Invoke_"); if (Invoke(objName, methodName, paras, resName)) { ret = Get(resName); Remove(resName); } return ret; } public object InvokeReturn(string objName, string methodName, string para) { object ret = null; string resName = GetUniqueName("Invoke_"); if (Invoke(objName, methodName, para, resName)) { ret = Get(resName); Remove(resName); } return ret; } public object InvokeReturn(string objName, string methodName) { object ret = null; string resName = GetUniqueName("Invoke_"); if (InvokeDirect(objName, methodName, (object[])null, resName)) { ret = Get(resName); Remove(resName); } return ret; } // Property public object GetPropertyDirect(string objName, string propertyName, object[] index) { object ret = null; string resName = GetUniqueName("Property_"); if (GetProperty(objName, propertyName, index, resName)) { ret = Get(resName); Remove(resName); } return ret; } public bool GetProperty(string objName, string propertyName, object[] index, string resName) { bool ret = false; object tar = Get(objName); Type type = null; if (tar != null) type = tar.GetType(); if (type != null) { PropertyInfo pi = type.GetProperty(propertyName); if (pi != null) { object res = pi.GetValue(tar, index); if (resName != null) Set(resName, res); ret = true; } } return ret; } public bool SetPropertyDirect(string objName, string propertyName, object[] index, object data) { bool ret = false; object tar = Get(objName); Type type = null; if (tar != null) type = tar.GetType(); if (type != null) { PropertyInfo pi = type.GetProperty(propertyName); if (pi != null) { pi.SetValue(tar, data, index); ret = true; } } return ret; } public bool SetProperty(string objName, string propertyName, object[] index, string dataName) { return SetPropertyDirect(objName, propertyName, index, Get(dataName)); } // Field public object GetFieldDirect(string objName, string fieldName) { object ret = null; string resName = GetUniqueName("Field_"); if (GetField(objName, fieldName, resName)) { ret = Get(resName); Remove(resName); } return ret; } public bool GetField(string objName, string fieldName, string resName) { bool ret = false; object tar = Get(objName); Type type = null; if (tar != null) type = tar.GetType(); if (type != null) { FieldInfo fi = type.GetField(fieldName); if (fi != null) { object res = fi.GetValue(tar); if (resName != null) Set(resName, res); ret = true; } } return ret; } public bool SetFieldDirect(string objName, string fieldName, object data) { bool ret = false; object tar = Get(objName); Type type = null; if (tar != null) type = tar.GetType(); if (type != null) { FieldInfo fi = type.GetField(fieldName); if (fi != null) { fi.SetValue(tar, data); ret = true; } } return ret; } public bool SetField(string objName, string fieldName, string dataName) { return SetFieldDirect(objName, fieldName, Get(dataName)); } // Global public object Get(string name) { return _globals[name]; } public void Set(string name, object data) { lock (_globals) { RemoveData(name); _globals.Add(name, data); } } public void Remove(string name) { lock (_globals) { RemoveData(name); } } private void RemoveData(string name) { if (_globals.ContainsKey(name)) _globals.Remove(name); } public string[] GetAllGlobalObjectName() { List<string> list = new List<string>(); foreach (string key in _globals.Keys) list.Add(key); return list.ToArray(); } }
趣味で、いらないメソッドが大量に切られている。
C#のスキルがそこそこあるなら、読む必要があるのは、Runメソッドのみ。
サンプル
次に利用する側のサンプルコード。
上記ライブラリーを参照設定する必要がある。
int ret = 0; AppDomain appDomain = AppDomain.CreateDomain(Compiler.GetUniqueName("AppDomain_")); Compiler compiler = null; if (appDomain != null) { Type type = typeof(Compiler); System.Reflection.Assembly asm = System.Reflection.Assembly.GetAssembly(type); using (compiler = appDomain.CreateInstanceAndUnwrap(asm.FullName, type.FullName) as Compiler) { if (compiler != null) { // ここからサンプル compiler.Set("RunTest", 55); // 実行するコードの断片 string code = @" int nTest = (int)c.Get(""RunTest"") * 10 + 3; c.Set(""RunTest"", nTest); "; // Runの第一引数は、compilerオブジェクトの名前(自身を渡してメソッドを呼ぶ) // 第二引数は、実行するコード断片 // 第三引数は、using句(usingを使うなら、多分、compiler.SetReferencedAssemblies()が必要 if (!compiler.Run("c", code, null)) ret++; // コード断片に対して値の受け渡しは、compilerの_globalsディクショナリー経由で行う if (!compiler.Get("RunTest").Equals(553)) ret++; } else ret++; } compiler = null; AppDomain.Unload(appDomain); } else ret++;
まー、見たまま理解して欲しいけど、、、retが0でなかったら、NGだった、ということね。
コード断片の「c」は、compiler自身。Runメソッドの第一引数で変更可能。
_globals["RunTest"]の値をintにキャストして、10倍+3した値をnTestに確保。
その値を_globals["RunTest"]に設定。
となっていて、Runして、結果期待値か検証しています。
実際にインタラクティブに何かするのであれば、using(compiler...の中で、何回もRunする想定でしょうか。
同じcompilerに対してなら、CompileSourceとかで独自クラスを作っての検証も可能ですし、やりたい放題かと。
きれいにしたくなったら、AppDomainから作り直し。
移動させたいAssemblyがあった時だけが微妙。Compilerクラスでソースを保持するか。
クラスを作成する場合は、こんな感じ。
こっちは、compilerオブジェクトを取得済みの想定。
int ret = 0; string source = @" using System; namespace Test { public class Sample { public Sample() { Console.WriteLine(""Sample constructor""); } public int _field; public int SampleProperty { get { return _field; } set { _field = value; } } public int SampleMethod(int a) { return a; } } } "; string asmName = compiler.CompileSource(source); if (asmName != null) { // constructor string objName = compiler.Create(asmName, "Test.Sample"); if (objName != null) { // method // Property // Field if (!compiler.InvokeDirectReturn(objName, "SampleMethod", 2547).Equals(2547)) ret++; if (!compiler.SetPropertyDirect(objName, "SampleProperty", null, 1436)) ret++; if (!compiler.GetFieldDirect(objName, "_field").Equals(1436)) ret++; if (!compiler.SetFieldDirect(objName, "_field", 325)) ret++; if (!compiler.GetPropertyDirect(objName, "SampleProperty", null).Equals(325)) ret++; if (compiler.GetPropertyDirect(objName, "SampleProperty", null).Equals(326)) ret++; if (compiler.GetFieldDirect(objName, "_field").Equals(1436)) ret++; // Run string code = "c.Set(\"RunTest\", 553);"; if (!compiler.Run("c", code, null)) ret++; } else { ret++; } } else { ret++; }
例は名前を指定しないメソッドばかり使ってますが、判りにくい名前になるので、極力指定した方が良いです。
PowershellのAdd-Typeは、削除できないので、Powershellの再起動が必要になる。
Powershellでライブラリーをロードして、サンプルの部分はスクリプト化すれば、インタラクティブな環境が出来上がりですね。
コンパイルもPowershellでやれば、スクリプトだけで可能でもある。
なお、Compilerを言語指定でインスタンス化すれば、その言語で動作します。
使える言語は、
List<string> ret = new List<string>(); CompilerInfo[] res = CodeDomProvider.GetAllCompilerInfo(); if (res != null) { for (int i = 0; i < res.Length; i++) { ret.AddRange(res[i].GetLanguages()); } }
あたりでどぞ。
普通は、csharp(c#/cs)/visualbasic(vb)/vbscript(vbs)/jscript(javascript/js)/vjsharp(vj#/vjs)/cpp(c++/mc) かな。
(独断と偏見順)
何が微妙だったか
いちいちDLLを作成するサンプルもあったが、それは嫌だった。
メモリー上で完結していたが、無駄なReflectionを含むのがあったので、整理した感じです。
とはいえ、私が書いたのが完璧かと言われれば、、、違うと言わざるおえない。
ま、使いやすいようにカスタマイズしてください。