如何执行外部未编译的 C# 代码






4.87/5 (11投票s)
从应用程序编译和运行 C# 代码。
引言
是否曾需要从应用程序运行外部脚本,并希望您的脚本具有 C# 等已编译语言的强大功能和速度?本“操作指南”将向您展示如何读取 C# 代码文件、编译它、检查错误以及执行代码。您甚至可以从已编译的应用程序中传递对象供外部代码使用。
背景
不时地,我曾需要通过让程序执行脚本来扩展我编写的程序,但我希望脚本能够使用应用程序内部的数据、日志记录等。这种方法使我能够使用 C# 的强大功能。在生产代码中,我会对 C# 代码文件使用校验和,以确保只有授权的脚本才能被编译和运行。
使用代码
这方面工作原理的核心可以在附件 ZIP 文件中的 CodeDOMProcessor.cs 类中找到。该项目展示了如何使用此类来读取为此示例设计的代码文件。
应注意,外部代码文件中的 using 语句使用文件名(例如,“System.dll”而不是“System”)。如果引用不在 GAC 中,则引用必须是程序集文件的完全限定文件名。一位用户报告说他不需要“.dll”扩展名,但在本文的初步开发(使用 VS2010)中,不包含它会导致运行时错误,因为无法解析引用的程序集。编译时收到的错误是
CompilerResults CompileResults = DOMProvider.CompileAssemblyFromSource(DOMCompilerParams, pCodeToCompile);
错误# [CS0006] - [找不到元数据文件 'System'] 行号# [0] 列号# [0]。
错误# [CS0006] - [找不到元数据文件 'System.Windows.Forms'] 行号# [0] 列号# [0]。
错误# [CS0006] - [找不到元数据文件 'System.Runtime.Serialization'] 行号# [0] 列号# [0]。
添加它并没有坏处,如果您不添加它,并且遇到此类异常,那么就添加“.dll”扩展名。
编写外部 C# 代码文件
显然,您要执行的代码必须像您项目中的代码一样可编译。下面显示的 C# 代码存在于示例项目 ZIP 文件 RunExternal.zip 中,名为 Test.cs。它包括一组“using”声明(对编译器没有意义),以及预期的命名空间、类和其他语句、声明和代码 - 甚至包括注释。我发现编译器期望类型名称是完整的名称,这让我相信 IDE 在“幕后”展开了这些名称。
<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">using System.dll;
using System.Windows.Forms.dll;
using System.Runtime.Serialization.dll;
namespace JeffJones.ExternalCode
{
public class JJScriptTest
{
public System.Int32 TestMemberInt = 0;
private System.String m_Message = "";
private System.String m_Caption = "";
/// <summary>
///
/// </summary>
/// <param name="pTestObject"></param>
/// <param name="pCaption"></param>
/// <param name="pTestObject"></param>
public void ShowMessage(System.String pMessage, System.String pCaption, RunExternal.TestObject pTestObject)
{
m_Message = pMessage;
m_Caption = pCaption;
System.String String2Show = pMessage + System.Environment.NewLine + pTestObject.ComputerName;
System.Windows.Forms.MessageBox.Show(String2Show, pCaption, System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Exclamation);
} // END public void ShowMessage(System.String pMessage, System.String pCaption, RunExternal.TestObject pTestObject)
/// <summary>
///
/// </summary>
/// <param name="pTestObject"></param>
public void ShowMessage2(RunExternal.TestObject pTestObject)
{
if ((m_Message.Length > 0) & (m_Caption.Length > 0))
{
System.String String2Show = m_Message + System.Environment.NewLine + pTestObject.ComputerName;
System.Windows.Forms.MessageBox.Show(String2Show, m_Caption, System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Exclamation);
}
else
{
throw new System.ArgumentException("Message and/or caption missing.");
}
} // END public void ShowMessage2(RunExternal.TestObject pTestObject)
/// <summary>
///
/// </summary>
public System.String Message2Show
{
get
{
return m_Message;
}
}
/// <summary>
///
/// </summary>
public System.String Caption
{
get
{
return m_Caption;
}
set
{
m_Caption = value;
}
}
} // END public class JJScriptTest
} // namespace JeffJones.ExternalCode
我通过 CodeFile.cs 类的实例方法“SetAndCompileCSCode()”逐行读取代码。在该方法中,我提取“using”声明以获取程序集引用,并且不将这些行包含在要编译的代码中。对于这个练习,我查找第一个类声明以获取“主类”名称(编译器必需的值)并使用它。改进此代码可以查找所有类名,以便用户可以选择要编译的主类。
CodeProvider 对象
在 CodeDOMProcessor.cs 类中,使用 Microsoft.CSharp.CSharpCodeProvider 对象(DOMProvider)来编译代码。System.CodeDom.Compiler.CompilerParameters 对象(DOMCompilerParams)与 CSharpCodeProvider 对象一起使用,以提供引用的程序集(外部代码顶部的“using”声明)。CSharpCodeProvider 的构造函数接受一个字典,该字典允许程序员指定要使用的 .NET 框架版本。对于这个示例项目,使用的是 .NET 4.0。
<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">DOMProviderOptions = new Dictionary<String, String>();
DOMProviderOptions.Add(COMPILER_VERSION_KEY, COMPILER_VERSION_SUPPORTED);
// Could use Microsoft.VisualBasic.VBCodeProvider for VB.NET code
// The Dictionary specifies the compiler version.
DOMProvider = new CSharpCodeProvider(DOMProviderOptions);
获取引用的程序集
在已经从代码中解析出程序集后,就可以将这些程序集添加到编译器中了。请记住,不在 GAC 中的引用必须是程序集文件的完全限定文件名。下面的代码展示了如何添加程序集。
<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">// Could use Microsoft.VisualBasic.VBCodeProvider for VB.NET code
// The Dictionary specifies the compiler version.
DOMProvider = new CSharpCodeProvider(DOMProviderOptions);
// Add referenced assemblies to the provider parameters
DOMCompilerParams = new CompilerParameters();
if (pReferencedAssemblies != null)
{
if (pReferencedAssemblies.Count > 0)
{
foreach (String RefAssembly in pReferencedAssemblies)
{
if (RefAssembly != null)
{
if (RefAssembly.Length > 0)
{
DOMCompilerParams.ReferencedAssemblies.Add(RefAssembly);
} // END if (File.Exists(pExecutableFullPath))
else
{
ReturnVal.Add(String.Format("A reference file was empty.{0}", Environment.NewLine));
}
} // END if (pExecutableFullPath.Length > 0)
else
{
ReturnVal.Add(String.Format("A reference file was null.{0}", Environment.NewLine));
}
} // END foreach (String RefAssembly in pReferencedAssemblies)
} // END if (pReferencedAssemblies.Count > 0)
} // END if (pReferencedAssemblies != null)
如果代码缺少所需的引用怎么办?您现在可以检查您知道需要的引用是否都存在,如果不存在,则添加它们。
<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">// These references will always be there to support the code compiling
// If these are not found, be sure to add them.
// Note references are in the form of the file name.
// If the reference is not in the GAC, you must supply the fully
// qualified file name of the assembly, such as C:\SomeFiles\MyDLL.dll.
if (!DOMCompilerParams.ReferencedAssemblies.Contains("System.dll"))
{
DOMCompilerParams.ReferencedAssemblies.Add("System.dll");
}
if (!DOMCompilerParams.ReferencedAssemblies.Contains("System.Windows.Forms.dll"))
{
DOMCompilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll");
}
if (!DOMCompilerParams.ReferencedAssemblies.Contains("System.Runtime.Serialization.dll"))
{
DOMCompilerParams.ReferencedAssemblies.Add("System.Runtime.Serialization.dll");
}
最后,添加外部代码本身的引用。
<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">// Adds this executable so it self-references.
DOMCompilerParams.ReferencedAssemblies.Add(System.Reflection.Assembly.GetEntryAssembly().Location);
现在编译器已选定,框架已选定,引用的程序集也已添加,应该选择编译后的代码的目标位置了。
内存中还是 DLL?
编译器可以在内存中生成结果,也可以创建文件。您可以设置编译器选项,选择是否包含调试信息,并指定主类。下面显示的 C# 代码使用了一个内存中的选项(尽管它也有一个文件名)。
<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">// For this example, I am generating the DLL in memory, but you could
// also create a DLL that gets reused.
DOMCompilerParams.GenerateInMemory = true;
DOMCompilerParams.GenerateExecutable = false;
DOMCompilerParams.CompilerOptions = "/optimize";
DOMCompilerParams.IncludeDebugInformation = true;
DOMCompilerParams.MainClass = pMainClassName;
// Compile the code.
CompilerResults CompileResults = DOMProvider.CompileAssemblyFromSource(DOMCompilerParams, pCodeToCompile);
System.CodeDom.Compiler.CompilerResults 实例包含编译结果,我们将在下面进行检查。
编译及结果
返回的 CompilerResults 实例包含一个 Errors 集合。您可以遍历该集合以查找阻止成功编译的错误。集合中的 CompileError 子对象提供了每个错误的行号和列号。请注意,在计算行号时,注释行不包含在编译器的行计数中。
<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">if (CompileResults.Errors.Count != 0)
{
foreach (CompilerError oErr in CompileResults.Errors)
{
ReturnVal.Add(String.Format("Error# [{0}] - [{1}] Line# [{2}] Column# [{3}].{4}",
oErr.ErrorNumber.ToString(), oErr.ErrorText, oErr.Line.ToString(),
oErr.Column.ToString(), Environment.NewLine));
} // END foreach (CompilerError oErr in CompileResults.Errors)
} // END if (CompileResults.Errors.Count != 0)
如果您愿意,可以像编译器一样维护一个单独的 String 列表来存储代码行,然后使用行号和列号向用户显示错误的精确位置。
你好,漂亮的代码 - 告诉我关于你的信息
编译器有很多信息可以告诉你关于代码代表的内容。这些信息对于决定是否使用外部代码或为用户提供使用它的选项非常有用。您可以获得有关以下方面的信息:
- Assemblies
- 构造函数和参数
- 成员(包括默认成员)
- 字段
- 方法、返回值和参数
下面显示的 C# 代码展示了这个例子如何使用对象来提取代码所代表的对象的 [信息]。您可以创建和填充包含从构造函数、方法、成员、属性等及其参数中提取的信息的类实例,而不是返回这些泛化的描述。
<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">Type[] ObjectTypes = CompileResults.CompiledAssembly.GetTypes();
// List<String> pMembers, List<String> pFields, List<String> pMethods, List<String> pProperties
if (ObjectTypes.Length > 0)
{
pFullTypeName = ObjectTypes[0].FullName;
Object CompiledObject = CompileResults.CompiledAssembly.CreateInstance(pFullTypeName);
Type CompiledType = CompiledObject.GetType();
pModuleName = CompiledType.Module.ScopeName;
// Beginning here, you could create and populate class instances that
// contain information gleaned from constructors, methods, members,
// properties, etc. and their parameters instead of passing back these
// generalized descriptions.
ConstructorInfo[] TempConstructors = CompiledType.GetConstructors();
foreach (ConstructorInfo TempConstructor in TempConstructors)
{
String StringToAdd = "";
if (TempConstructor.Name == ".ctor")
{
StringToAdd = "void " + ObjectTypes[0].Name;
}
else
{
StringToAdd = "void " + TempConstructor.Name;
}
String ParmString = "";
if (TempConstructor.Module.ScopeName.Equals(pModuleName))
{
ParameterInfo[] TempConstructorParam = TempConstructor.GetParameters();
foreach (ParameterInfo TempParam in TempConstructorParam)
{
ParmString += String.Format("{0} {1}, ", TempParam.ParameterType.FullName, TempParam.Name);
}
}
StringToAdd += "(" + ParmString + ")";
pConstructors.Add(StringToAdd);
}
MemberInfo[] TempDefaultMembers = CompiledType.GetDefaultMembers();
// List<String> pFields, List<String> pMethods, List<String> pProperties
if (TempDefaultMembers.Length > 0)
{
foreach (MemberInfo TempMember in TempDefaultMembers)
{
if (TempMember.Module.ScopeName.Equals(pModuleName))
{
String StringToAdd = "";
StringToAdd = String.Format("{0} {1}, ", TempMember.ReflectedType.FullName, TempMember.Name);
pMembers.Add(StringToAdd);
} // END if (TempMember.Module.ScopeName.Equals(pModuleName))
} // END if (TempDefaultMembers.Length > 0)
} // END if (TempDefaultMembers.Length > 0)
FieldInfo[] TempFields = CompiledType.GetFields();
// List<String> pFields, List<String> pMethods, List<String> pProperties
if (TempFields.Length > 0)
{
foreach (FieldInfo TempField in TempFields)
{
if (TempField.Module.ScopeName.Equals(pModuleName))
{
String StringToAdd = "";
StringToAdd = String.Format("{0} {1}, ", TempField.ReflectedType.FullName, TempField.Name);
pFields.Add(StringToAdd);
} // END if (TempField.Module.ScopeName.Equals(pModuleName))
} // END foreach (FieldInfo TempField in TempFields)
} // END if (TempFields.Length > 0)
MemberInfo[] TempMembers = CompiledType.GetMembers();
// List<String> pProperties
if (TempMembers.Length > 0)
{
foreach (MemberInfo TempMember in TempMembers)
{
if (TempMember.Module.ScopeName.Equals(pModuleName))
{
String StringToAdd = "";
StringToAdd = TempMember.ToString(); // String.Format("{0} {1}, ", TempMember.GetType().FullName, TempMember.Name);
pMembers.Add(StringToAdd);
}
} // END if (TempDefaultMembers.Length > 0)
} // END if (TempDefaultMembers.Length > 0)
MethodInfo[] TempMethods = CompiledType.GetMethods();
foreach (MethodInfo TempMethod in TempMethods)
{
if ((TempMethod.Module.ScopeName.Equals(pModuleName)) && (!TempMethod.IsSpecialName))
{
String StringToAdd = "";
StringToAdd = String.Format("{0} {1}, ", TempMethod.ReturnType.FullName, TempMethod.Name);
ParameterInfo[] TempParams = TempMethod.GetParameters();
String ParmString = "";
foreach (ParameterInfo TempParam in TempParams)
{
String ParamName = TempParam.Name;
String ParamTypeName = TempParam.ParameterType.FullName;
Object DefaultValue = TempParam.DefaultValue;
if (DefaultValue.ToString().Length == 0)
{
ParmString += String.Format("{0} {1}, ", ParamTypeName, ParamName);
}
else
{
ParmString += String.Format("{0} {1}={2}, ", ParamTypeName, ParamName, DefaultValue.ToString());
}
} // END foreach (ParameterInfo TempParam in TempParams)
if (ParmString.EndsWith(", "))
{
ParmString = ParmString.Substring(0, ParmString.Length - 2);
}
StringToAdd += "(" + ParmString + ")";
pMethods.Add(StringToAdd);
} // END if (TempMethod.Module.ScopeName.Equals(pModuleName))
} // END foreach (MethodInfo TempMethod in TempMethods)
PropertyInfo[] TempProperties = CompiledType.GetProperties();
// List<String> pProperties
if (TempProperties.Length > 0)
{
foreach (PropertyInfo TempProperty in TempProperties)
{
if (TempProperty.Module.ScopeName.Equals(pModuleName))
{
String StringToAdd = "";
StringToAdd = String.Format("{0} {1}, ", TempProperty.PropertyType.FullName, TempProperty.Name);
if (TempProperty.CanRead && TempProperty.CanWrite)
{
StringToAdd += " (get/set)";
}
else if (!TempProperty.CanRead && TempProperty.CanWrite)
{
StringToAdd += " (set ONLY)";
}
else if (TempProperty.CanRead && !TempProperty.CanWrite)
{
StringToAdd += " (get ONLY)";
}
else
{
// No action
}
pProperties.Add(StringToAdd);
} // END if (TempProperty.Module.ScopeName.Equals(pModuleName))
} // END if (TempDefaultMembers.Length > 0)
} // END if (TempDefaultMembers.Length > 0)
} // END if (ObjectTypes.Length > 0)
下面是示例应用程序中显示的 [结果]:
执行代码
成功编译代码后,剩下的就是执行它了。最后一步非常简单。您创建已编译程序集的实例,获取对要执行的方法的引用,然后使用对象数组参数调用该方法,并捕获返回值。下面显示的 C# 代码展示了如何为这个例子完成这项工作。对于这个例子,只有一个程序集。外部文件可能有更多。
<pre style="background: white; color: black; font-family: Consolas; font-size: 13px;">Type[] ObjectTypes = CompileResults.CompiledAssembly.GetTypes();
if (ObjectTypes.Length > 0)
{
String FullTypeName = ObjectTypes[0].FullName;
Object CompiledObject = CompileResults.CompiledAssembly.CreateInstance(FullTypeName);
MethodInfo CompiledMethod = CompiledObject.GetType().GetMethod(pExecutionMethodName);
Object ReturnValue = CompiledMethod.Invoke(CompiledObject, pMethodParameters);
} // END if (ObjectTypes.Length > 0)
现在您有了外部未编译的 C# 代码,这些代码已从 C# 程序中加载、编译和运行。
安全怎么办?
如果开发人员是脚本的唯一提供者和维护者,那么使用“晚期绑定”或通过反射读取的 DLL 显然比可编辑脚本更好。但是,如果脚本是由开发人员以外的人(例如支持团队或其他足够了解 C# 或 VB.NET 来维护脚本的人)创建或维护的,那么防止恶意脚本就变得很重要。这种情况是本节关于安全性的背景。
一个合理且谨慎的问题可能是:“我如何防止有人提供恶意脚本,而我的程序却要为它造成的损害负责?”
有几种方法,我将在本文中介绍其中一种。
校验和方法 - 当您提供脚本时,您可以对每一行加上您的程序才知道的秘密字符串进行校验和。您的代码的第一行是一个注释,其中包含脚本创建时计算的总校验和值。如果程序在读取代码时计算的校验和(减去第一行)与文件中的值匹配,则文件有效。如果不匹配,则代码不会被编译或执行。
结论
当您想到能够为您编写的程序创建或提供脚本所提供的通用性时,这种能力为扩展您的应用程序提供了一种新的方式。
关注点
通过使用 Microsoft.VisualBasic.VBCodeProvider 处理 VB.NET 代码,这也适用于 VB.NET 代码。
历史
何时何地谁
======= == ======================================================
2014 年 8 月 5 日 JDJ Genesis。
2014 年 9 月 5 日 JDJ 更新,文本扩展。