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

от автора

Жил — был древний код эпохи динозавров

Дано: адов кодярник работающий с 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; 

не меняется.

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

Литовский праздник «обломайтис»

Запускаем тесты. БАЦ!

Не, пока все вызовы того кома возвращают 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 }); 

По идее — ничего?

Галантерейщик и кардинал — это сила

А вот и меняется. Тест снова пройден. И что делать? Превращать такой красивый код в макароны — не улыбается, да и много его.

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

Пробуем.

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; 	} 

Вот теперь — победа.

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

ссылка на оригинал статьи http://habrahabr.ru/post/252371/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *