4

A) 即时编译 C# EXE 和 DLL 相对容易。
B) 执行一个 EXE 意味着一个新的应用程序正在运行。加载 DLL 意味着可以在应用程序或项目之间共享的情况下使用方法和函数。

现在,可以从MSDN或为了您的方便找到编译您的 EXE(或稍作修改,DLL)的最快和最简单的方法:

private bool CompileCSharpCode(string script)
{
lvErrors.Items.Clear();
    try
    {
        CSharpCodeProvider provider = new CSharpCodeProvider();
        // Build the parameters for source compilation.
        CompilerParameters cp = new CompilerParameters
        {
            GenerateInMemory = false,
            GenerateExecutable = false, // True = EXE, False = DLL
            IncludeDebugInformation = true,
            OutputAssembly = "eventHandler.dll", // Compilation name
        };

        // Add in our included libs.
        cp.ReferencedAssemblies.Add("System.dll");
        cp.ReferencedAssemblies.Add("System.Windows.Forms.dll");
        cp.ReferencedAssemblies.Add("Microsoft.VisualBasic.dll");

        // Invoke compilation. This works from a string, but you can also load from a file using FromFile()
        CompilerResults cr = provider.CompileAssemblyFromSource(cp, script);
        if (cr.Errors.Count > 0)
        {
            // Display compilation errors.
            foreach (CompilerError ce in cr.Errors)
            {
                //I have a listview to display errors.
                lvErrors.Items.Add(ce.ToString());
            }
            return false;
        }
        else
        {
            lvErrors.Items.Add("Compiled Successfully.");
        }
        provider.Dispose();
    }
    catch (Exception e)
    {
        // never really reached, but better safe than sorry?
        lvErrors.Items.Add("SEVERE! "+e.Message + e.StackTrace.ToString());
        return false;
    }
    return true;
}

现在您可以即时编译,如何加载 DLL 之间存在一些差异。通常来说,您会将它作为参考添加到 Visual Studios 中以编译到项目中。这很简单,你可能已经做过很多次了,但是我们想在我们当前的项目中使用它,我们不能很好地要求用户每次想要测试他们的新 DLL 时都重新编译整个项目. 因此,我将简单讨论如何“动态”加载库。这里的另一个术语是“以编程方式”。为此,在成功编译后,我们加载一个程序集,如下所示:

Assembly assembly = Assembly.LoadFrom("yourfilenamehere.dll");

如果你有一个 AppDomain,你可以试试这个:

Assembly assembly = domain.Load(AssemblyName.GetAssemblyName("yourfilenamehere.dll"));


现在 lib 已被“引用”,我们可以打开它并使用它。有两种方法可以做到这一点。一个要求您知道该方法是否有参数,另一个会为您检查。后面我会做,其他的你可以查看MSDN

// replace with your namespace.class
Type type = assembly.GetType("company.project");
if (type != null)
{
    // replace with your function's name
    MethodInfo method = type.GetMethod("method");

    if (method != null)
    {
        object result = null;
        ParameterInfo[] parameters = method.GetParameters();
        object classInstance = Activator.CreateInstance(type, null);
        if (parameters.Length == 0) // takes no parameters
        {
                // method A:
            result = method.Invoke(classInstance, null);
                // method B:
            //result = type.InvokeMember("method", BindingFlags.InvokeMethod, null, classInstance, null);
        }
        else // takes 1+ parameters
        {
            object[] parametersArray = new object[] { }; // add parameters here

                // method A:
            result = method.Invoke(classInstance, parametersArray);
                // method B:
            //result = type.InvokeMember("method", BindingFlags.InvokeMethod, null, classInstance, parametersArray);
        }
    }
}

问题:第一次编译工作正常。第一次执行工作正常。但是,重新编译尝试会出错,说明您的 *.PDP(调试器数据库)正在使用中。我听说过一些关于编组和 AppDomains 的提示,但我还没有完全解决这个问题。仅在加载 DLL 后重新编译才会失败。


编组 && AppDomain 的当前尝试:

class ProxyDomain : MarshalByRefObject
    {
        private object _instance;
        public object Instance
        {
            get { return _instance; }
        }
        private AppDomain _domain;
        public AppDomain Domain
        {
            get
            {
                return _domain;
            }
        }
        public void CreateDomain(string friendlyName, System.Security.Policy.Evidence securityinfo)
        {
            _domain = AppDomain.CreateDomain(friendlyName, securityinfo);
        }
        public void UnloadDomain()
        {
            try
            {
                AppDomain.Unload(_domain);
            }
            catch (ArgumentNullException dne)
            {
                // ignore null exceptions
                return;
            }
        }
        private Assembly _assembly;
        public Assembly Assembly
        {
            get
            {
                return _assembly;
            }
        }
        private byte[] loadFile(string filename)
        {
            FileStream fs = new FileStream(filename, FileMode.Open);
            byte[] buffer = new byte[(int)fs.Length];
            fs.Read(buffer, 0, buffer.Length);
            fs.Close();

            return buffer;
        }
        public void LoadAssembly(string path, string typeName)
        {
            try
            {
                if (_domain == null)
                    throw new ArgumentNullException("_domain does not exist.");
                byte[] Assembly_data = loadFile(path);
                byte[] Symbol_data = loadFile(path.Replace(".dll", ".pdb"));

                _assembly = _domain.Load(Assembly_data, Symbol_data);
                //_assembly = _domain.Load(AssemblyName.GetAssemblyName(path));
                _type = _assembly.GetType(typeName);
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException(ex.ToString());
            }
        }
        private Type _type;
        public Type Type
        {
            get
            {
                return _type;
            }
        }
        public void CreateInstanceAndUnwrap(string typeName)
        {
            _instance = _domain.CreateInstanceAndUnwrap(_assembly.FullName, typeName);
        }
    }

_instance = _domain.CreateInstanceAndUnwrap(_assembly.FullName, typeName); 上的错误 说我的程序集不可序列化。尝试将 [Serializable] 标签添加到我的班级,但没有成功。仍在研究修复。

当你看不到它们是如何被使用时,似乎事情会变得有点混乱,所以这里让它变得简单吗?

private void pictureBox1_Click(object sender, EventArgs e)
    {
        pd.UnloadDomain();

        if (CompileCSharpCode(header + tScript.Text + footer))
        {
            try
            {
                pd.CreateDomain("DLLDomain", null);
                pd.LoadAssembly("eventHandler.dll", "Events.eventHandler");
                pd.CreateInstanceAndUnwrap("Events.eventHandler"); // Assembly not Serializable error!

                /*if (pd.type != null)
                {
                    MethodInfo onConnect = pd.type.GetMethod("onConnect");

                    if (onConnect != null)
                    {
                        object result = null;
                        ParameterInfo[] parameters = onConnect.GetParameters();
                        object classInstance = Activator.CreateInstance(pd.type, null);
                        if (parameters.Length == 0)
                        {
                            result = pd.type.InvokeMember("onConnect", BindingFlags.InvokeMethod, null, classInstance, null);
                            //result = onConnect.Invoke(classInstance, null);
                        }
                        else
                        {
                            object[] parametersArray = new object[] { };

                            //result = onConnect.Invoke(classInstance, parametersArray);
                            //result = type.InvokeMember("onConnect", BindingFlags.InvokeMethod, null, classInstance, parametersArray);
                        }
                    }
                }*/
                //assembly = Assembly.LoadFrom(null);

            }
            catch (Exception er)
            {
                MessageBox.Show("There was an error executing the script.\n>" + er.Message + "\n   - " + er.StackTrace.ToString());
            }
            finally
            {
            }
        }
    }
4

2 回答 2

9

将 DLL 加载到正在运行的进程(的默认应用程序域)后,磁盘上的文件在进程终止之前不能被覆盖。DLL 不能像在非托管代码中一样在托管代码中卸载。

您需要在主机进程中创建一个新的应用程序域,并将新创建的 DLL 程序集加载到该应用程序域中。当您准备好编译新版本的 DLL 时,您可以处置 appdomain。这将从内存中卸载 DLL 并释放对 DLL 文件的锁定,以便您可以将新的 DLL 编译到同一文件。然后,您可以构建一个新的 appdomain 以将新的 DLL 加载到其中。

使用 appdomains 的主要危险是必须对跨 appdomain 边界的所有调用进行编组,很像 IPC 或网络 RPC。尽量将需要跨 appdomain 边界调用的对象的接口保持在最低限度。

您还可以将程序集编译到内存,接收字节数组或流作为输出,然后将该程序集加载到单独的应用程序域中。这样可以避免在磁盘上产生最终需要删除的碎片。

不要使用编译到内存作为文件锁定问题的解决方法。核心问题是程序集在加载到进程的默认应用程序域时无法从内存中删除。如果您想在进程生命周期的后期从内存中卸载该程序集,则必须创建一个新的 appdomain 并将 DLL 加载到该 appdomain 中。

以下是如何在另一个 appdomain 的上下文中构造对象的粗略概述:

    var appdomain = AppDomain.CreateDomain("scratch");
    byte[] assemblyBytes = // bytes of the compiled assembly
    var assembly = appdomain.Load(assemblyBytes);
    object obj = appdomain.CreateInstanceAndUnwrap(assembly.FullName, "mynamespace.myclass");

在此序列之后,obj将包含对链接到 appdomain 内实际对象实例的代理的引用。您可以使用反射或将 obj 类型转换为通用接口类型来调用 obj 上的方法,并直接调用方法。准备进行调整以支持方法调用参数的 RPC 编组。(请参阅 .NET 远程处理)

使用多个应用程序域时,您必须小心访问类型和程序集的方式,因为许多 .NET 函数默认在调用者的当前应用程序域中运行,这通常不是您拥有多个应用程序域时想要的。compilerResult.CompiledAssembly例如,在调用者的 appdomain 中内部执行生成的程序集的加载。您想要的是将程序集加载到您的其他应用程序域中。你必须明确地这样做。

更新:在您最近添加的显示您如何加载应用程序域的代码片段中,这一行是您的问题:

 _assembly = Assembly.LoadFrom(path);

这会将 DLL 加载到当前的 appdomain(调用者的 appdomain)中,而不是加载到目标 appdomain(在您的示例中由 _domain 引用)。您需要使用_domain.Load()将程序集加载到应用程序域中。

于 2013-04-04T15:39:44.107 回答
2

如果您不需要调试,或者不介意调试“动态”代码,但缺少一些信息。您可以在内存中生成代码..这将允许您多次编译代码..但不会生成 .pdb

cp.GenerateInMemory = true;

或者,如果您不需要能够在磁盘上找到程序集,您可以要求编译器转储临时目录中的所有代码并为 dll 生成一个临时名称(这将始终是唯一的)

cp.TempFiles = new TempFileCollection(Path.GetTempPath(), false);
//cp.OutputAssembly = "eventHandler.dll";

在这两种情况下,要访问 dll 及其类型,您都可以从编译器结果中获取它

Assembly assembly = cr.CompiledAssembly; 

不需要显式加载

但是如果不适用这种情况,并且您必须在已知文件夹中使用带有 .pdp 的物理 .dll .. 我可以给您的唯一建议是在 dll 上放置版本号.. 如果您不这样做有一种简单的方法来控制编译 dll 的次数,您可以随时使用时间戳。

cp.OutputAssembly = "eventHandler"+DateTime.Now.ToString("yyyyMMddHHmmssfff")+".dll";

当然,您必须意识到,每次编译新的 .dll 时都会将其加载到内存中并且不会被卸载,除非您使用单独的应用程序域..但这超出了这个问题的范围..

于 2013-04-04T15:48:56.393 回答