...

пятница, 6 марта 2015 г.

Грабли, .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;
}



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


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


This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

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