Грабли, .NET, COM и dynamic

ae234fa38d8d4109a43f6f3e4b3c6832.pngЖил — был древний код эпохи динозавровДано: адов кодярник работающий с 16ю разными версиями одного и того же «ах какого» продукта. COM, Interop, интерфейсы, реализации, сигнлтоны с факторями, паттерны с антипаттернами, модули и прочие ошметки крывавого ынтырпрайзу. Стандартный набор. Рос, мужал и матерел тот кодярник лет семь. Пока однажды очередной фикс не привел к исправлению массового копипаста в 16 модулях. Если кому интересно — foreach на for меняли.Помучившись, провели исследование. Копипаст на 95% идентичен, различаются только имена пакетов из интеропов.

А можно ли как-то писать так чтобы не оборачивать сотни и сотни функций в свои врапперы, плюс ручками боксинг / анбоксинг этих врапперов?

Есть же ключевое слово dynamic!

И тогда адские макароны вот такого чудесного вида

стандартный ужастик public abstract class Application: IDisposable { public abstract void Close (); public abstract Document CreateDocument (); public abstract Document OpenDocument (string doc_path);

// еще 200 методов // куча пропертей типа версий, путей и так далее void IDisposable.Dispose () { Close (); } }

public class ClientApplication: Application { protected ClientApplication (){ string recovery_path = Environment.GetFolderPath (Environment.SpecialFolder.LocalApplicationData); recovery_path = Path.Combine ( recovery_path, String.Format ( @»…\Version {0}\en_GB\Caches\Recovery», Version));

try { foreach (string file in Directory.GetFiles (recovery_path)){ try { File.Delete (file); } catch { } } } catch {}

// еще подпорок из палок и веревок

}

public override void Close () { if (Host!= null) { Marshal.ReleaseComObject (Host); Host = null; } } }

public class ClientApplication7_5: ClientApplication { protected ClientApplication7_5() { Type type = Type.GetTypeFromProgID (»…Application.» + Version, true); _app = Activator.CreateInstance (type) as Interop75.Application; Host = app; // … }

public override Document CreateDocument () { return new ClientDocument7_5(this, _app.Documents.Add ()); }

public override Document OpenDocument (string doc_path) { return new ClientDocument7_5(this, _app.Open (doc_path, true, …) as Interop75.Document); }

// и еще 200 врапперов

public override ComObject Host { get { return _app; } set { _app = value as Interop75.Application; } } private Interop75.Application _app; // и еще пропертей с версиями прог-айди и прочим }

public class ServerApplication: Application { public ServerApplication () {} … }

// та же трава что и для клиент аппликейшен, еще 8 раз становится ненужными, а код который это безобразие использовал var app = Factory.GetApplication (); var doc = app.Documents.Add ();

doc.DocumentPreferences.PreserveLayoutWhenShuffling = false; doc.DocumentPreferences.AllowPageShuffle = true; doc.DocumentPreferences.StartPageNumber = 1; не меняется.

Профит? Ура, работает! Два десятка мегабайт полунагенеренного ужастика удачно выкидываем в мусорку. Поддержка новых версий радикально упрощается.

410bf4463fed4271825ba5c062cd0ba4.png

Литовский праздник «обломайтис» Запускаем тесты. БАЦ! Не, пока все вызовы того кома возвращают OK — то и работает тоже супер. Но стоило дождаться теста

try { var app = Factory.GetApplication (); var doc = app.Documents.Add ();

doc.DocumentPreferences.PreserveLayoutWhenShuffling = false; doc.DocumentPreferences.AllowPageShuffle = true; doc.DocumentPreferences.StartPageNumber = -1; } catch (COMException ok) { … // должны быть тут и красиво в лог записать «нишмагла» } catch (Exception bad) { … // мы вот тут, а bad — это NullReferenceException БЕЗ StackTrace!!! } Шок, скандалы, интриги, расследования. Если кому интересно — подтвержденный баг в микрософте, пофикшен будет не ранее 5.0. Грустно и скучно.

Пытливый ум не дает покоя — ведь если ходить через интеропы то там все как надо? Отладчик показывает тип нашего документа как System.__ComObject. А как же RCW? Просто не вычислило?

Меняем тест на

try { var app = Factory.GetApplication (); var doc = app.Documents.Add () as Interop75.Document;

doc.DocumentPreferences.PreserveLayoutWhenShuffling = false; doc.DocumentPreferences.AllowPageShuffle = true; doc.DocumentPreferences.StartPageNumber = -1; } catch (COMException ok) { … // и мы опять на своем месте } catch (Exception bad) { … } и… тест пройден.Гипотеза интересна. Так может оно просто не может вычислить тип? Проверяем

var app = Factory.GetApplication (); var doc = app.Documents.Add ();

var typeName = Microsoft.VisualBasic.Information.TypeName (doc); Хм хм. Вполне себе.Идеи закончились.

Но постойте — есть же сырцы? Смотрим, курим, восхищаемся мастерству запутывания. Начали отсюда: __ComObject. Плавно перетекли сюда: Type.cs. Закончили ildasm. В процессе курева пришло понимание — так там явно несколько мест обрабатывающих эти комы по разному. А что будет если заменить

doc.DocumentPreferences.StartPageNumber = -1; на Type type = doc.DocumentPreferences.GetType (); type.InvokeMember («StartPageNumber», BindingFlags.SetProperty, null, doc.DocumentPreferences, new object[] { -1 }); По идее — ничего? a27a545d0af844cb8f138312bb9f7fbf.png

Галантерейщик и кардинал — это сила А вот и меняется. Тест снова пройден. И что делать? Превращать такой красивый код в макароны — не улыбается, да и много его.Поздно, вечер, пытаюсь толсто потроллить и разрядить обстановку — так может свою реализацию динамиков подсунем — на рефлектах? Еще не закончив мысль понимаю —, а это мысль!

Пробуем.

ComWrapper extends DynamicObject public class ComWrapper: DynamicObject { public ComWrapper (object comObject) { _comObject = comObject; _type = _comObject.GetType (); }

public object WrappedObject { get { return _comObject; } } // вдруг кому будет надо

// стандартно пропертя гет + сет public override bool TryGetMember (GetMemberBinder binder, out object result) { result = Wrap (_type.InvokeMember (binder.Name, BindingFlags.GetProperty, null, _comObject, null)); return true; }

public override bool TrySetMember (SetMemberBinder binder, object value) { _type.InvokeMember ( binder.Name, BindingFlags.SetProperty, null, _comObject, new object[] { Unwrap (value) }); return true; }

// та же трава про вызов метода public override bool TryInvokeMember (InvokeMemberBinder binder, object[] args, out object result) { result = Wrap (_type.InvokeMember ( binder.Name, BindingFlags.InvokeMethod, null, _comObject, args.Select (arg => Unwrap (arg)).ToArray () )); return true; }

// наш ручной боксинг — анбоксинг private object Wrap (object obj) { return obj!= null && obj.GetType ().IsCOMObject? new ComWrapper (obj) : obj; }

private object Unwrap (object obj) { ComWrapper wrapper = obj as ComWrapper; return wrapper!= null? wrapper._comObject: obj; }

// очевидно то что нам передали в конструкторе + тип переданного чтобы сто раз не считать private object _comObject; private Type _type; } Прекрасно — все делает сам, работает как надо, все что нужно — это обернуть им результат Factory.GetApplication (). Прямо там и оборачиваем. Есть правда нюанс — забыли про коллекции. Так что чуть погодя добавили еще и такое:

еще немного подпорок // наш енумератор на коленке private IEnumerable Enumerate () { foreach (var item in (IEnumerable)_comObject) yield return Wrap (item); }

// автоконвертация к enumerable public override bool TryConvert (ConvertBinder binder, out object result) { if (binder.Type.Equals (typeof (IEnumerable)) && _comObject is IEnumerable) { result = Enumerate (); return true; } result = null; return false; }

// и поддержка работы как с массивом, по индексу. На всякий случай public override bool TryGetIndex (GetIndexBinder binder, object[] indexes, out object result) { if (indexes.Length == 1) { dynamic indexer = _comObject; result = Wrap (indexer[indexes[0]]); return true; }

result = null; return false; }

public override bool TrySetIndex (SetIndexBinder binder, object[] indexes, object value) { if (indexes.Length == 1) { dynamic indexer = _comObject; indexer[indexes[0]] = Unwrap (value); return true; } return false; } Вот теперь — победа.

Вдруг кому пригодится.

© Habrahabr.ru