Создание "движка" для выполния псевдо-скрипта, назовем его CSharp Script
Программист рано или поздно сталкивается с проблемой интеграции в свои приложения скриптов как инструмента расширения функционала. Многие используют jscript, vbscript, ms scripting host в общем, также lua и т.д.
Необходимость разработки трехзвенки с гибким и легкорасширяемым сервером, просто модульной платформы или (упоси бог) учетки с БЛ на клиенте ведет к попыткам взаимодействовать с подобными скриптами, или созданию механизма плагинов. Имея уже сравнительно большой опыт разработки систем, активно использующих механизм плагинов, я решил добавить возможность создавать сценарии для своего сервера в виде именно скриптов (псевдо-скриптов на самом деле) на родном для платформы языке (C#) без вкручивания костылей на msc или других скриптах.
Учитывая наличие пронстранств имен System.CodeDom.Compiler и Microsoft.CSharp мы имеем возможность как компилировать исходный код, так и выполнять его, загружать в домены приложения и т.д.
Прелюдия:
Это помогут сделать классы CSharpCodeProvider, CompilerParameters и CompilerResults.
Итак, создаем новый проект (моя версия FrameWork >=2.0), "Windows exe".
Я назвал класс, занимающийся отработкой "команд" препоцессора CSharpScriptHost. Наш хост немного расширяет возможности C# - я добавил туда директивы (парсингу синтаксиса время не уделялось, будет желание - можно добавить)
#include - подключение к файлу скрипта дополнительных исходных файлов для компиляции, имя файла пишется без кавычек.
#inline - вставка в исходный код содержимого указанного файла, имя файла без кавычек
#reference - подключение дополнительных сборок, объекты и пространства имен которых будут использованы в скрипте
Реализация класса CSharpScriptHost (сильно не пинайте - с архитектурой не заморачивался, и есть поле для оптимизации):
Код: Выделить всё
using System;
using System.CodeDom.Compiler;
using System.IO;
using System.Reflection;
using System.Text;
using System.Windows.Forms;
using Microsoft.CSharp;
using System.Collections.Generic;
namespace CSharpScript
{
public class CSharpScriptHost
{
/// <summary>
/// Создает объект CSharpScriptHost
/// </summary>
public CSharpScriptHost() {
Parameters = new Dictionary<string, object>();
}
public Dictionary<string, object> Parameters;
/// <summary>
/// Показывать окно ожидания в процессе сборки?
/// </summary>
public bool ShowWindow { get; set; }
/// <summary>
/// Выполняет файл, указанный в filename
/// </summary>
/// <param name="filename">Имя файла</param>
/// <returns></returns>
public bool ExecuteScriptFromFile(string filename)
{
return ExecuteScript(GetFile(filename));
}
/// <summary>
/// Загружает текстовый файл в строковую переменную
/// </summary>
/// <param name="filename"></param>
/// <returns></returns>
private string GetFile( string filename) {
string code;
try {
StreamReader reader = new StreamReader( filename );
code = reader.ReadToEnd();
reader.Close();
reader.Dispose();
reader = null;
}
catch ( FileNotFoundException fnf ) {
throw new CSharpScriptHostException( fnf.Message );
}
return code;
}
private List<string> references;
/// <summary>
/// Обработка кода до сборки, формирование списка требующихся дополнительных сборок и выполнение
/// команд препроцессора
/// </summary>
/// <param name="code">Код скрипта</param>
/// <returns></returns>
private string ParseCode(string code) {
StringBuilder resCode = new StringBuilder();
foreach ( string line in code.Split( '\n') ) {
string cmd = line.Trim();
string[] asm = line.Replace( ";", String.Empty ).Trim().Split( new char[] { ' ', '\t' }, 2 );
if ( cmd.StartsWith( "#reference" ) ) {
if ( asm.Length > 1 )
references.Add( asm[1].Trim() );
else
throw new CSharpScriptHostException( "#reference: Ожидается имя сборки." );
continue;
}
if ( cmd.StartsWith( "#include" ) ) {
if(asm.Length > 1){
string file = GetFile( asm[1] );
string inccode = ParseCode( file );
resultCode.Add( inccode);
}
else
throw new CSharpScriptHostException( "#include: Ожидается имя файла." );
continue;
}
if ( cmd.StartsWith( "#inline" ) ) {
if ( asm.Length > 1 ) {
string file = GetFile( asm[1] );
resCode.Append(ParseCode( file));
}
else
throw new CSharpScriptHostException( "#inline: Ожидается имя файла." );
continue;
}
resCode.Append(line);
}
return resCode.ToString();
}
private List<string> resultCode;
/// <summary>
/// Выполняет скрипт
/// </summary>
/// <param name="code">Код скрипта</param>
/// <returns></returns>
public bool ExecuteScript(string code)
{
references = new List<string>();
resultCode = new List<string>();
FormWait frmWait = new FormWait();
if ( ShowWindow ) {
frmWait.Show();
frmWait.Update();
}
resultCode.Add(ParseCode( code ));
CSharpCodeProvider provider = new CSharpCodeProvider();
string filename = Path.GetTempFileName();
CompilerParameters parameters = new CompilerParameters();
parameters.GenerateInMemory = true;
parameters.GenerateExecutable = false;
parameters.OutputAssembly = filename;
foreach ( string asm in references ) {
parameters.ReferencedAssemblies.Add( asm );
}
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
if(parameters.ReferencedAssemblies.IndexOf(asm.ManifestModule.Name) <0 )
parameters.ReferencedAssemblies.Add(asm.Location);
CompilerResults result = provider.CompileAssemblyFromSource(parameters, resultCode.ToArray());
frmWait.Close();
frmWait.Dispose();
frmWait = null;
StringBuilder errorInfo = new StringBuilder();
if(result.Errors.Count > 0)
errorInfo.Append("При сборке возникли ошибки:\n");
foreach (CompilerError error in result.Errors)
{
if (error.IsWarning)
continue;
errorInfo.Append(error.ToString());
errorInfo.Append("\n");
}
if (!string.IsNullOrEmpty(errorInfo.ToString()))
{
throw new CSharpScriptHostException( errorInfo.ToString());
}
else
{
if(File.Exists(filename))
File.Delete(filename);
Type t = result.CompiledAssembly.GetType( "ScriptMain.ScriptMain" );
MethodInfo m = null;
if ( t != null ) {
m = t.GetMethod( "Main" );
if ( m != null ) {
m.Invoke( null, new object[] {Parameters } );
}
else
throw new CSharpScriptHostException( "Метод static void Main(Dictionary<string,object>) не найден!" );
}
else
throw new CSharpScriptHostException( "Не объявлен класс ScriptMain.ScriptMain!" );
}
return true;
}
}
}
Код: Выделить всё
using System;
namespace CSharpScript {
public class CSharpScriptHostException:Exception {
public CSharpScriptHostException( string message ) : base( message ) { }
}
}
В результате мы получили класс, умеющий компилировать и выполнять код из "псевдо-скрипта" из обычной строковой переменной. Откуда возьмется код в этой переменной - нам абсолютно не важно. Также он умеет загружать код из файла или нескольких файлов с использованием директив.
Обратите внимание, что хост добавляет к параметрам компилятора ссылки на все сборки, которые использует основной домен приложения. Это говорит о том, что вы можете свободно использовать в скрипте объекты и, например, поля, статических классов приложения - достаточно только указать соответствующий using.
Для чего делалось "Windows exe" ?
Привожу простой код startup-класса (там для гламура нужно лишние юзинги вычистить):
Код: Выделить всё
using System;
using System.CodeDom.Compiler;
using System.IO;
using System.Reflection;
using System.Text;
using System.Windows.Forms;
using Microsoft.CSharp;
using System.Collections.Generic;
namespace CSharpScript
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
CSharpScriptHost scriptHost = new CSharpScriptHost();
scriptHost.ShowWindow = true;
for ( int i = 0; i < args.Length; i++ )
scriptHost.Parameters.Add( String.Format( "arg_{0}", i.ToString() ), args[i] );
if (args.Length < 1)
{
MessageBox.Show("Не указано имя файла.", Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
try {
scriptHost.ExecuteScriptFromFile( args[0] );
}
catch ( CSharpScriptHostException ex ) {
MessageBox.Show( ex.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error );
}
}
}
}
Для того, чтобы использовать объект CSharpScriptHost в своем приложении - просто добавляем референс на получившуюся сборку, и вперед, расширяйте функционал без пересборки проекта, шифруйте исходные файлы "скриптов" как угодно если боитесь открытого кода, или этого не позволяет политика фирмы.. В общем, полная свобода действий. Наслаждайтесь!
P.S. О защите: если вас не устраивает то, что скрипт имеет возможность равноправно взаимодействовать с кодом приложения - загружайте сборку скрипта в отдельный AppDomain и не добавляйте ссылки на сборки, да и защиту на "фильтр" подключаемых из скрипта сборок сделать - 5 минут.
В прикреленном архиве проект для MSVS и пример скрипта в папке Debug.
Если вам это пригодилось, или заинтересовало вас - я готов рассказывать о различных интересных для разработчика моментах и дальше.