Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Динамическое Прототипирование

15 комментариев

Мне всегда нравилась возможность редактирования программ прямо в процессе их исполнения. Основная мотивация в том, что хочется корректировать мелкие ошибки ничего не перезапуская. Ну и добавлять функционал тоже хочется. В этом посте про то как все это делается. На CodeProject эту идею явно не поняли, не знаю будет ли лучше тут.

Ах да – исходный код тут: http://bitbucket.org/nesteruk/dynamicprototyping

Идея

Суть того что я покажу в том, чтобы редактировать программу прямо во время исполнения. Если вы считаете что это делает там Edit & Continue и аналогичные вещи, вы явно не в теме – для E&C нужна студия, работает это только в x86, оно несовместимо с Linq, и так далее. Предлагать DLR для этих целей бессмысленно – нет объективных причин использовать для этой цели DLR вместо CLR.

Компиляция

Сначала о самом простом – как скомпилировать сборку “внутрипроцессно” и подменить ее? Для этого нужно чтобы:

  • В сборке был только один тип
  • Все референсы правильно резолвились
  • Не было привязки к местоположению сборки (Assembly.Location или как там)

Даже выполнив эти условия, косяков будет масса, поэтому главная идея – в редактировании изолированных компонентов. Иначе будет больно (хотя я иногда себе и это позволяю).

Итак, для начала нужно сформировать набор параметров для компиляции, воткнув в ссылки все возможные сборки.

private static CompilerParameters PrepareCompilerParameters()
{
  var ps = new CompilerParameters { GenerateInMemory = true, GenerateExecutable = false };
  // add everything in this AppDomain
  foreach (var reference in AppDomain.CurrentDomain.GetAssemblies())
  {
    try
    {
      ps.ReferencedAssemblies.Add(reference.Location);
    } 
    catch (Exception ex)
    {
      Debug.WriteLine("Cannot add assembly " + reference.FullName + " as reference.");
    }
  }
  return ps;
}

Некоторые сборки не добавятся в силу своей злостности. Там есть свои проблемы с тем что можно и нельзя добавлять, советую это определять индивидуально.

Теперь можно уже что-то компилировать. Идея простая – компилируем всё в памяти и возвращаем первый найденный тип. Надеюсь он единственный.

internal object CompileAndInstantiate(string sourceCode, string compilerVersion = "v4.0")
{
  CompilerParameters ps = PrepareCompilerParameters();
  var po = new Dictionary<string, string> { { "CompilerVersion", compilerVersion } };
  var p = new CSharpCodeProvider(po);
  var results = p.CompileAssemblyFromSource(ps, new[] { sourceCode });
  if (results.Errors.HasErrors)
  {
    var sb = new StringBuilder();
    foreach (var e in results.Errors)
      sb.AppendLine(e.ToString());
    throw new Exception(sb.ToString());
  }
  var ass = results.CompiledAssembly;
  var mainType = ass.GetTypes()[0];
  return Activator.CreateInstance(mainType);
}

Пример выше утрированный, тут вам никаких настроек компилятора толком нет. Но тем не менее он полностью рабочий. Тип компилируется и возвращается.

Редактирование

Тут все просто – программа знает где исходники для определенного типа. Если вам проще, используйте convention over configuration. А можно просто запускать и передавать absolute path исходника. Проблемы есть, но не говорите мне что задача не масштабируется.

Так вот, теперь насчет редактора. Вообще-то можно использовать TextBox, но я использую более прогрессивные методы. Мораль такая: редактировать можно в чем угодно, но если у вас есть выход на платные контролы – стоит пользоваться. Получите IntelliSense впридачу. А что – удобно.

В принципе, все инкапсулируется в отдельное диалоговое окно которое можно показывать. А использование его просто:

var dlg = new CSharpEditorWindow(@"..\..\Converter.cs");
if (dlg.ShowDialog() == DialogResult.OK)
{
  converter = (IConverter) dlg.Result;
}

Иначе говоря, окошко само все компилирует, нужно только брать результат. Ну и естественно если не скомпилируете, получите ExceptionMessageBox. Можно было бы поразвивать инфраструктуру, но мне откровенно лень.

Что с этим делать?

Идея простая: если у вас крутится код который не стоит трогать, можно менять его прямо в процессе исполнения! Очень полезно! Впрочем, убеждать не буду – ведь юнит-тесты, E&C и прочие прелести никто не отменял. Просто работа с ними, собственно как и использование прямой перекомпиляции – занятие нужное и не слишком эффективное.

Вот в принципе и все. Если вы совсем не поняли что я написал и для чего это надо – не страшно. А если вам хочется получить полный редактор с IntelliSense-образными плюшками – стукните мне по скайпу.

Реклама

Written by Dmitri

11 ноября 2010 в 1:03

Опубликовано в .NET, C#

комментариев 15

Subscribe to comments with RSS.

  1. Могли бы вы привести реальный пример, зачем это может понадобиться? Т.к. идея конечно интересная, но мне не очень понятно где и зачем это можно применить.

    Вадим

    11 ноября 2010 at 9:37

    • Да везде. Серьезно, например разрабатываю я алгоритм для TypograFix’а. Вдруг понимаю что мне нехватает фичи Х, дописать которую быстрее чем перекомпилировать и перезапустить проект. Беру, открываю редактор, добавляю фичу, закрываю. Все работает, сорцы обновились — в следующий раз, фича будет скомпилированна «нативно».

      Dmitri

      11 ноября 2010 at 11:44

  2. Получается, мы меняем исходник, компилируем его, получая новую сборку в памяти каждый раз, когда меняем код. Таким образом, все объекты этого типа так и продолжают ссылаться на тип из родной сборки (а не из новой), пока вы их не переопределите. Это еще больше ограничивает область применения этой фичи.

    Evgeny

    11 ноября 2010 at 16:45

  3. Это я к тому, что вы пишете про «подмену» сборки, хотя на самом деле это не так, и каждый раз создается новая сборка, не имеющая связи с родной.

    Evgeny

    11 ноября 2010 at 16:47

    • Даже выполнив эти условия, косяков будет масса, поэтому главная идея – в редактировании изолированных компонентов.

      Позвольте пояснить. Вы редактируете некий ServiceImplementor, который реализует IService. Далее, где-то в вашем когде, у вас написано

      [Dependency] IService service;
      

      или что-то вроде того. Все ажурно. Теперь вы вдруг захотели изменить ServiceImplementor вручную. Никаких проблем. Вы получили, скажем так, ServiceImplementor2, а далее, все что вам нужно сделать – это присвоить соответствуюшее поле:

      this.service = (IService)dlg.Result;
      

      Вот собственно и все.

      Dmitri

      11 ноября 2010 at 23:53

  4. Феерично! Уже много раз, когда читаешь заголовок твоей статьи, первая мысль — это же НЕВОЗМОЖНО! Уважуха.

    ulu

    12 ноября 2010 at 9:26

  5. Решил послушать Ваши старые подкасты, но не один не запускается, видно что то случилось с rpod.ru, потмоу что ссылки, как я понял, ведут именно туда.
    Сорри что пишу сюда, думаю здесь Вы чаще проверяете комментарии. Спасибо за блог и подкаст.

    Максим

    15 ноября 2010 at 10:25

  6. Насчет IntelliSense и редактора — постоянно пользуются редактором из SharpDevelop 3.x, он бесплатен и используется во многих проектах, в т.ч. не имеющих отношения к C# (FlashDevelop, например)

    Станислав

    15 ноября 2010 at 11:29

    • Также для определения исходников можно использовать Mono.Cecil.Pdb, Mono.Cecil.Mdb, а также Mono.Cecil.Debugger для перевода в C# код, компилятор — для обратного перевода.

      Станислав

      15 ноября 2010 at 11:31

    • Да, AvalonEdit, знаю. Но мне легче взять что-то совсем готовое, где идеально настроен C#.

      Dmitri

      16 ноября 2010 at 10:38

  7. У нас был года два назад проект, где основная идея была схожа с идеей вашей статьи. Мы тоже использовали Actipro SyntaxEditor(косяков у него тоже хватает, но и возможностей предостаточно) Из внешней системы(что-то на подобии e-commerce) шла лавиной инфа в наше приложение, эта инфа маппилась на наши бизнесс entities, которые отображились в гридах операторам. Фишка в том, что рулы на отображение этих бизнесс entities задавались, как динамически скомпиленные классы, которые оператор мог подредактировать в Actipro SyntaxEditor:
    public class Rule1
    {
    public bool IsVisible
    {
    get
    {
    if (price > 1000 & amount > 5)
    return true;
    return false;
    }
    }
    }
    Потом он жмякал кнопку скомпилить, апплаил рулы и отображение данных на гридах менялось.

    BorodaAlex

    30 ноября 2010 at 16:53

    • Да, я помню как я аналогичные вещи делал на WF (еще 3й версии) — это было весело. А идея-то аналогичная. Жаль что с WF не срослось…

      Dmitri

      30 ноября 2010 at 17:00

  8. Дмитрий вы продвигаете идею «редактировать программу прямо во время исполнения», но вам не кажется, что сам C# для этого не подходит, а точнее компилятор от MS. Вот один из моментов описанный Эриком Липпертаном (Eric Lippert)http://blogs.msdn.com/b/ericlippert/archive/2010/03/29/putting-a-base-in-the-middle.aspx (перевод http://blogs.msdn.com/b/ruericlippert/archive/2010/07/21/putting-a-base-in-the-middle.aspx )

    Леонид

    30 декабря 2010 at 11:50

    • Хотел ответить в ваш пост «Горячая подмена в .Net приложениях», но так как читал обе статьи по неосторожности написал сюда.

      Леонид

      30 декабря 2010 at 11:55

    • Я читал этот пост. В принципе у меня пока таких ситуаций не возникало, и те механизмы что я тут описываю работают и оправдывают свое существование. Более того, поскольку все это я делаю только когда занимаюсь тестированием программы, в Release ничего из этого не попадает.

      Dmitri

      30 декабря 2010 at 17:44


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: