您的位置:首页 > 编程语言 > C#

c#动态编译,自己做了个c#脚本管理框架,可以作为其他应用的插件使用。增强程序的拓展性

2017-05-25 21:01 871 查看
一直在CSDN汲取养分,学到了很多东西,抽个时间也反哺一下。

c#动态编译

1、初衷

       最近在做一个物联网服务器的cs版,由于业务的可变性太大,每次去更新正在运行的服务器不仅会带来一定的隐患,而且还麻烦。所以就考虑到做一个插件,可以动态的拓展业务,而不会对以前的功能造成影响。所以就想到了动态编译,但是论坛里关于动态编译的帖子太少,完成不了自己的需求,索性抽点时间自己造个轮子。水平有限,欢迎指教。




2、效果展示



插件框架目录结构



3、详细

        1>、c#编译器

       c#提供了一套动态编译的库CSharpCodeProvider,具体编译过程我就不啰嗦了,网上一搜一大堆。我对其做了二次封装,方便调用,代码如下。(后面均是给予该封装进行编译操作)
class DynamicCompiler
{
/// <summary>
/// 设定要编译的代码及要引用的外部dll
/// </summary>
/// <param name="pdlls">依赖的包名</param>
/// <returns>CompilerParameters</returns>
public static CompilerParameters setCompilerParameters(helper.ProvideDLL pdlls)
{
// Sets the runtime compiling parameters by crating a new CompilerParameters instance
CompilerParameters objCompilerParameters = new CompilerParameters();
//  objCompilerParameters.ReferencedAssemblies.Add("System.dll");

// Load the remote loader interface
foreach (string item in pdlls.Dlls)
objCompilerParameters.ReferencedAssemblies.Add(item);
// Load the resulting assembly into memory
//objCompilerParameters.GenerateInMemory = false;
return objCompilerParameters;
}
/// <summary>
/// 编译
/// </summary>
/// <param name="param">编译依赖参数</param>
/// <param name="sourceCode">源码</param>
/// <param name="intoMemory">是否写入内存</param>
/// <param name="outPutName">输出编译文件名称(dll)</param>
/// <returns></returns>
public static CompilerResults compile(CompilerParameters param,string sourceCode,bool intoMemory,string outPutName=null)
{
CSharpCodeProvider objCSharpCodePrivoder = new CSharpCodeProvider();
param.GenerateInMemory = intoMemory;
if(outPutName!=null)
{
param.GenerateExecutable = false;
param.OutputAssembly = outPutName;
}
return objCSharpCodePrivoder.CompileAssemblyFromSource(param, sourceCode);
}
/// <summary>
/// 多文件编译
/// </summary>
/// <param name="param"></param>
/// <param name="files">文件地址列表</param>
/// <param name="intoMemory"></param>
/// <param name="outPutName"></param>
/// <returns></returns>
public static CompilerResults compile(CompilerParameters param, string []files, bool intoMemory, string outPutName = null)
{
CSharpCodeProvider objCSharpCodePrivoder = new CSharpCodeProvider();
param.GenerateInMemory = intoMemory;
if (outPutName != null)
{
param.GenerateExecutable = false;
param.OutputAssembly = outPutName;
}
return objCSharpCodePrivoder.CompileAssemblyFromFile(param, files);
}

/// <summary>
/// 通过Dll创建运行示例,该示例就是用户编译代码的类的实例,
/// 创建成功后,会将该实例加入到用户指定程序域运行
/// </summary>
/// <param name="domain">程序区</param>
/// <param name="sourceName">编译文件名称</param>
/// <param name="className">类名</param>
/// <param name="constructArgs">类构造函数参数</param>
/// <returns></returns>
public static object creatRunningObj( CDomain domain,string sourceName,string className, object[] constructArgs)
{
RemoteLoaderFactory factory = (RemoteLoaderFactory)domain.MAppDomain.CreateInstance("RemoteAccess", "RemoteAccess.RemoteLoaderFactory").Unwrap();
// with help of factory, create a real 'LiveClass' instance
return factory.Create(sourceName, className, constructArgs);
}
/// <summary>
/// 通过CompilerResults创建运行实例
/// </summary>
/// <param name="cr"></param>
/// <param name="className"></param>
/// <returns></returns>
public static object creatRunningObj(CompilerResults cr, string className)
{
if (cr.Errors.HasErrors)
throw new DynamicCompile.exceptions.DynamicComplierNullObjectException("传入了一个错误的CompilerResults");
return cr.CompiledAssembly.CreateInstance(className);
}
/// <summary>
/// 获取程序集内所有的类
/// </summary>
/// <param name="cr"></param>
/// <returns></returns>
public static Type[]getClasses(CompilerResults cr)
{
if (cr.Errors.HasErrors)
throw new DynamicCompile.exceptions.DynamicComplierNullObjectException("传入了一个错误的CompilerResults");
return cr.CompiledAssembly.GetTypes();
}
}
作为一个工具,代码比较简单,研究过c#动态编译的基本上拿过来就能用。

2>、插件框架设计思想

    大致过程为: 脚本-编译器-控制器-执行引擎-执行结果。再对每层做抽象,个人习惯使用抽象类。在脚本层方面,因为脚本编译器是c#编译器,所以在脚本内部只能是c#语言,至于lua、Python等脚本,以后即使想要引入本框架,只要实现相应的引擎即可。引入方法可以是文件引入、串引入。后续。大致框架思想如下(跟实际实现会有出入,只是前期简单的画了下) 



 该图可对应上述框架目录。关于抽象控制器AbstractController和抽象脚本示例,我的想法是,控制器是要率先启动的,他并不知道后面还有多少脚本要加入运行。且在宿主进程运行过程中可以随时杀掉该附属脚本进程来保证宿主进程的稳定运行。所以中间抽象一层AbstractCScript,继承该类的所有脚本拥有本框架识别的同一出入口函数,在宿主进程中也可以提供相应的界面管理当前脚本系统已加载的所有脚本。未继承的脚本当然也会在控制器内出现,在主进程中也可以考虑把他们的管理加上。当然控制器的所有逻辑均根据个人需求实现,我在抽象控制器中实现了一些基本操作接口,例如加载和卸载管理,继承和非继承对象管理等,关键性接口均是可以复写的。我也写了个测试控制器如testController.
代码如下:
/***********************************************************************************************************
* name:AbstractController
* 描述:抽象编译控制器,功能:指定文件路径脚本编译、指定依赖路径、指定输出路径,过滤已编译的文件,扫描程序集所有可用类并生成实例
* 作者:che
* 时间:2017-05-25
* *********************************************************************************************************/
using DynamicCompile.exceptions;
using DynamicCompile.helper;
using DynamicCompile.process;
using RemoteAccess;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DynamicCompile.controller
{
abstract class AbstractController
{
#region proteries
protected string _scriptPath ;                                                                                 //该控制器要加载的脚本路径
protected string _dllsPath;                                                                                    //依赖dll所在路径
protected string _scriptSuffix ;                                                                               //脚本文件后缀,现指定为cs文件
protected CDomain _mCDomain;                                                                                   //用户定义程序域
protected string _outPutName ;                                                                                 //编译输出dll基础名称,匹配自定义命名规则
protected int _outDllCount;                                                                                    //输出DLL计数
protected Dictionary<string, IRemoteInterface> _abscScriptDic;                                                 //className/*-hashCode*/ - AbstractCScript脚本实例
protected Dictionary<string, object> _otherScriptDic ;                                                         //className/*-hashCode*/ - 其他脚本实例
protected Dictionary<int, string> _compiledFiles ;                                                             // files_fullName.hashCode-filesFullName    已编译的文件
# endregion
public abstract void Initialize();
public AbstractController()
{
Initialize();
}
#region  Method
/// <summary>
/// 关闭并释放资源
/// </summary>
public virtual  void shutDown()
{
this.deleteAllOutDll();                                                                                      //清空所有输出文件
this._abscScriptDic.Clear();                                                                                 //清空所有执行实例
this._otherScriptDic.Clear();
this._compiledFiles.Clear();                                                                                 //清除所有编译文件记录
this._mCDomain.shutDowm();                                                                                   //关闭程序域
this.Dispose();
}

/// <summary>
/// 删除所有输出文件
/// </summary>
protected virtual void deleteAllOutDll()
{
while ((this._outDllCount--) > 0)                                                                                 //输出文件命名规则和删除规则相同
DynamicCompile.helper.AbstractFileUtils.delete(_outPutName + _outDllCount + ".dll");
}
/// <summary>
/// 增加编译文件记录
/// </summary>
/// <param name="files"></param>
protected void addCompileFilesRecord(string[] files)
{
foreach (string item in files)
{
if (this._compiledFiles.ContainsKey(item.GetHashCode()))
throw new ControllerException("已编译过该文件!" + item);
this._compiledFiles.Add(item.GetHashCode(), item);
}
}
/// <summary>
/// 扫描并编译指定文件下的脚本
/// </summary>
public virtual void scanAndCompile()
{
string[] files = AbstractFileUtils.filterFiles(AbstractFileUtils.getFilesName(this._scriptPath, this.ScriptSuffix), this._compiledFiles.Keys.ToArray());//加载所有指定目录的脚本,并过滤掉已编译的文件
string[] dlls = helper.AbstractFileUtils.getFilesName(this._dllsPath, "*.dll");
if (files.Length == 0)
return;

CompilerParameters param = DynamicCompiler.setCompilerParameters(new helper.ProvideDLL(dlls));              //加载所有DLL
CompilerResults cr = DynamicCompiler.compile(param, files, false, getOutDllName());                         //采用dll输出方式,方便程序管理
string txt = stringFormartUtils.format(cr);                                                                 //失败则输出编译结果
if (txt.Length != 0)
throw new DynamicCompile.exceptions.DynamicCompileException(txt);
AbstractLogUtil.GetLogger().LogInfo("complie sucess,outPut:" + cr.CompiledAssembly.CodeBase + "\n class:" + DynamicCompiler.getClasses(cr).ToString());
addCompileFilesRecord(files);
Type[] classes = DynamicCompiler.getClasses(cr);                                                            //拿到程序集的所有类
foreach (Type item in classes)
{
compileClass(item, cr);                                                                                 //现在仅实现了对类的支持,后续可以支持接口、抽象类
compileAbstractt(item, cr);
compileAbstractt(item, cr);
}
}
/// <summary>
/// 根据类名,在传入程序集寻找并创建类的实体记录
/// </summary>
/// <param name="item"></param>
/// <param name="cr"></param>
protected virtual void compileClass(Type item, CompilerResults cr)
{
if (item.IsClass != true) return;
if (this._abscScriptDic.ContainsKey(item.Name) || this._otherScriptDic.ContainsKey(item.Name))
return;
object ob = null;
try
{
ob = DynamicCompiler.creatRunningObj(this._mCDomain, cr.CompiledAssembly.CodeBase, item.FullName, null);
this._abscScriptDic.Add(item.Name, ob as IRemoteInterface);                                           //加入到实体缓存列表方便重复使用
}
catch (Exception e)
{
ob = DynamicCompiler.creatRunningObj(cr, item.FullName);                                              //使用cr创 建类时,可以加一下判定
this._otherScriptDic.Add(item.Name, ob);
DynamicCompile.helper.AbstractLogUtil.GetLogger().LogWarn("非继承AbstractCScript" + item.Name);
}
//object ob = DynamicCompiler.creatRunningObj(cr, item.FullName);                                         //使用cr创 建类时,可以加一下判定
//AbstractCScript ab =ob as AbstractCScript;
//if (typeof(AbstractCScript).IsInstanceOfType(ob) != true)                                               //如果不是继承了本实例的类
//    continue;

}
private  void compileInterface(Type item, CompilerResults cr)
{ }
private void compileAbstractt(Type item, CompilerResults cr)
{ }
/// <summary>
///输出文件名称获取接口,
///没有做文件检查,因为在删除的时候,即使多次调用该函数却没有产生实际输出,也不会造成什么影响。
/// </summary>
/// <returns></returns>
protected virtual string getOutDllName()
{
return this._outPutName + (this._outDllCount++) + ".dll";
}
#endregion
#region IDisposable Support
private bool disposedValue = false; // 要检测冗余调用

public string ScriptSuffix { get => _scriptSuffix; set => _scriptSuffix = value; }

protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: 释放托管状态(托管对象)。
}

// TODO: 释放未托管的资源(未托管的对象)并在以下内容中替代终结器。
// TODO: 将大型字段设置为 null。

disposedValue = true;
}
}

// TODO: 仅当以上 Dispose(bool disposing) 拥有用于释放未托管资源的代码时才替代终结器。
// ~TestController() {
//   // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
//   Dispose(false);
// }

// 添加此代码以正确实现可处置模式。
public void Dispose()
{
// 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
Dispose(true);
// TODO: 如果在以上内容中替代了终结器,则取消注释以下行。
// GC.SuppressFinalize(this);
}
#endregion
}
}
测试控制器
using DynamicCompile.helper;
using DynamicCompile.process;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RemoteAccess;
using DynamicCompile.exceptions;
using System.IO;
using System.Drawing;

namespace DynamicCompile.controller
{
class TestController:AbstractController,IDisposable
{

public  override void Initialize()
{
_scriptPath = @"..\..\DynamicCompile\files";                                                                    //该控制器要加载的脚本路径
_dllsPath = @"..\..\DynamicCompile\dlls";                                                                       //依赖dll所在路径
ScriptSuffix = "*.cs";                                                                                         //脚本文件后缀,现指定为cs文件
_mCDomain = new CDomain("testDomain");                                                                          //用户定义程序域
_outPutName = @"..\..\DynamicCompile\dlls\OutDll_";                                                             //编译输出dll名称
_outDllCount=0;                                                                                                 //输出DLL计数
_abscScriptDic = new Dictionary<string, IRemoteInterface>();                                                    //className/*-hashCode*/ - AbstractCScript脚本实例
_otherScriptDic = new Dictionary<string, object>();                                                             //className/*-hashCode*/ - 其他脚本实例
_compiledFiles = new Dictionary<int, string>();                                                                 // files_fullName.hashCode-filesFullName    已编译的文件
AbstractLogUtil.SetLogger(new DefaultLogger(), LogLevel.Info);
}

public void compTest()
{
this.scanAndCompile();
}
public string test()
{
StringBuilder msg = new StringBuilder();

string classname = "class1";
if (this._abscScriptDic.ContainsKey(classname))
{
CSObj obj = new CSObj(_abscScriptDic[classname],"say", new object[1] { "testController" }, true);
Object robj=  DynamicCompile.engine.AbstractEngine.creatEngine(engine.ENGINE_TYPE.CS).run(obj);
msg.AppendLine(robj as string);
Console.WriteLine(robj);
}

classname = "class3";
CSObj ob = new CSObj(_otherScriptDic[classname], "say", new object[1] { "testController" }, false);
Object rob = DynamicCompile.engine.AbstractEngine.creatEngine(engine.ENGINE_TYPE.CS).run(ob);
Console.WriteLine(rob);
msg.AppendLine(rob as string);
classname = "class3_son1";
ob = new CSObj(_otherScriptDic[classname], "say", new object[1] { "testController" }, false);
rob = DynamicCompile.engine.AbstractEngine.creatEngine(engine.ENGINE_TYPE.CS).run(ob);
Console.WriteLine(rob);
msg.AppendLine(rob as string);
return msg.ToString();
}
/// <summary>
///输出文件名称获取接口,
///没有做文件检查,因为在删除的时候,即使多次调用该函数却没有产生实际输出,也不会造成什么影响。
/// </summary>
/// <returns></returns>
private string getOutDllName()
{
return this._outPutName + (this._outDllCount++) + ".dll";
}

}
}
三个测试class;
using DynamicCompile.helper;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CompileTest.DynamicCompile.files
{
class class1:AbstractCScript
{
public class2 class2 = new class2();
public string say(string hello)
{
return new class3().say("class1") + "\n" + "class1: " + hello;
}
}
}

namespace CompileTest.DynamicCompile.files
{
class class3
{
public int age = 456;
public string say(string he)
{
return "class3:hello " + he;
}
public class class3_son1
{
public int age = 123;
public string say(string he)
{
return "class3_son1:hello " + he;
}
}
}
}


关于实用性方面,脚本间相互调用时,需要多文件编译编译器内部有接口。在脚本使用宿主工程的某些资源时,需要先将宿主资源编译成库,例如我会在我的解决方案下新建一个类库项目,项目添加所有工程类的引用(当然也可以用到什么引用什么,但是以后脚本如果用到其他没有编译的资源时,就要更新该库。)如下MLibrary所示:




如图我在hello 中引用了根目录下的testClass2的静态函数getNum和test2目录下的hello2的sayHello函数,







最后是控制器测试,测试控制器代码上面有了,这个是控制器使用 的。files包里的三个测试class代码上面有,里面展示了在脚本间的相互调用,以及内部类的使用。结果如下
TestController contrl = new TestController();
private void button3_Click(object sender, EventArgs e)
{
contrl.scanAndCompile();
}

private void button4_Click(object sender, EventArgs e)
{
string msg= contrl.test();
txtResult.Text =DateTime.Now+"\n"+ msg;
}




总结:整个东西花了两天,写个博文却花了两三个小时

,可能是我语言功底太差。水平有限,欢迎指点。里面还有一些想法没有实现,也欢迎大家提出更好的建议与意见。资源上传居然没有搞好

,后续再开源吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐