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


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

    Вдруг кому пригодится.
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 7

      +3
      Красиво, спасибо. Пишите еще, стиль приятный, мысли толковые.
        +3
        Читал и плакал. Обязательно пишите ещё!
          0
          Дайте ссылку на баг чтоль.

          А костыль эпичный получился.
            0
            Я бы и рад — но повторное гугление не дало вернуть ту тему на форуме микрософта где «ну это все-же наше» ;-(
            +5
            Стиль хорош, но все-таки есть непонятные места. Я вроде в контексте, но уж очень много пропусков в логических цепочках. В конце как минимум нужно дописать то, что получилось в виде «а теперь мы можем сделать то-то и то-то таким вот образом». И пример кода, который работает.
              0
              Тогда уж «работает как работало но теперь на стероидах динамиках».

              А насчет пропусков — вы спрашивайте, я попробую пояснить. Повествование еще кроме стандартного «проблема — решение» пытается показать полет мысли при поиске того самого решения, вероятно это сбивает с толку 8-)

              Сырцы уж извините — вырезать лобзиком долго и нудно.
              +1
              А мне на 3.5 нравилось через Emit сложные обертки генерить…

              Only users with full accounts can post comments. Log in, please.