動的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を含むのがあったので、整理した感じです。


とはいえ、私が書いたのが完璧かと言われれば、、、違うと言わざるおえない。
ま、使いやすいようにカスタマイズしてください。