Error-checking smart pointer
Apr. 25th, 2016 07:58 pmКак вы, вероятно, знаете, в мире Windows до сих пор COM живее всех живых, и поскольку его должно быть можно использовать и из Си, работа с ошибками там построена на базе кодов возврата: функции обычно возвращают HRESULT, целое число, которое 0 если все ок, отрицательно при ошибке и положительно, если это как бы не ошибка, но и не совсем полный успех. Ошибки надлежит не игнорировать и не пропускать, как-то обрабатывать, однако очень часто вся обработка сводится к тому, чтобы прекратить выполнение текущей ф-ии и передать эту ошибку наверх. В языках вроде Go это превращается в код вида
и т.д. Каждая строчка превращается в четыре, в лучших традициях

В C и C++ обычно та же фигня, но можно обложиться макросами, которые будут прятать проверку и return внутрь себя или даже бросать исключение (в случае С++). Когда-то давно мой старый С++ код, работающий с возвращающими HRESULT функциями, выглядел как-то так:
или, если надо было добавить к ошибке текст,
макросы эти бросали исключение специальнго типа, а верхний код содержал пару других макросов, которые умели ловить разные ошибки и исключения и конвертить их к общему виду для единообразной обработки.
Что занятно, язык Rust пришел к тому же. Там есть стандартный тип для возвращения "результат или ошибка", и чтобы сделать последовательность вызовов, передавая ошибку наверх при первой встрече, пишут как-то так:
где try! - это макрос, разворачивающийся в if/return. Букв, конечно, меньше чем в Go, но все равно выглядит безобразно.
Вот в языках с родной поддержкой монад (Haskell, Idris) код может выглядеть годно:
Точно так же коды возврата проверяются, и при обнаружении ошибки выполнение прерывается, и ошибка передается наверх, однако весь boilerplate спрятан в реализацию монады, при этом забыть проверить ошибку не получится.
В моем недавнем проекте на D нужно было активно работать с COM, и захотелось тоже иметь такой механизм обработки кодов возврата, чтобы с одной стороны не захламлял код в типичных сценариях, а с другой стороны не позволял забыть проверку. И получилось сделать вот так:
Т.е. это smart pointer, похожий на знакомый С++ программистам CComPtr, который так же как там занимается COM-овким делом подсчета ссылок (вызывает AddRef() / Release() когда следует), и позволяет вызывать методы того типа, на который содержит указатель, но в отличие от С++ не просто вызывает их, а заодно проверяет возвращенное значение и бросает нужное исключение в случае ошибки. Возможно это благодаря тому, что в D есть opDispatch - это как method_missing в Ruby, но полностью статически проверенный и скомпилированный. За основу я взял ComPtr из исходников VisualD и основательно по нему прошелся. Выглядит это как-то так:
Неявное приведение смартпоинтера к указываемому типу я убрал, если нужен исходный голый указатель, надо явно писать
fn - это строковая compile-time переменная. Соответственно,
возвращает она строку вроде "vs[0], vs[1].raw, vs[2].raw, vs[3]", и эту строку opDispatch выше вставляет в код вызова метода.
Если нужно обработать ошибку не способом по-умолчанию, а иначе, достаточно добавить raw:
Если же нужно не просто выбросить ошибку наверх, а еще снабдить ее дополнительным сообщением, то выглядит это так:
Казалось бы, как может
превращается в
а второе позволяет отложить вычисление ее аргумента:
Т.е. вызов
Такое вот очередное применение статической интроспекции и метапрограмминга. Полный исходник модуля здесь.
err := obj1.Action1(a, b)
if err != nil {
return err
}
err := obj1.Action2(c, d)
if err != nil {
return err
}
err := obj2.Action3(e)
if err != nil {
return err
}и т.д. Каждая строчка превращается в четыре, в лучших традициях

В C и C++ обычно та же фигня, но можно обложиться макросами, которые будут прятать проверку и return внутрь себя или даже бросать исключение (в случае С++). Когда-то давно мой старый С++ код, работающий с возвращающими HRESULT функциями, выглядел как-то так:
AHRCK(obj1.Action1(a, b)); AHRCK(obj1.Action2(c, d)); AHRCK(obj2.Action3(e));
или, если надо было добавить к ошибке текст,
AHRCK2(obj1.Action1(a, b), "Memory drum fell off and rolled away"); AHRCK2(obj1.Action2(c, d), "Printer is on fire"); AHRCK2(obj2.Action3(e), "This is a user error. Certainly. Not mine.");
макросы эти бросали исключение специальнго типа, а верхний код содержал пару других макросов, которые умели ловить разные ошибки и исключения и конвертить их к общему виду для единообразной обработки.
Что занятно, язык Rust пришел к тому же. Там есть стандартный тип для возвращения "результат или ошибка", и чтобы сделать последовательность вызовов, передавая ошибку наверх при первой встрече, пишут как-то так:
try!(obj1.action1(a, b)); try!(obj1.action2(c, d)); try!(obj2.action3(e));
где try! - это макрос, разворачивающийся в if/return. Букв, конечно, меньше чем в Go, но все равно выглядит безобразно.
Вот в языках с родной поддержкой монад (Haskell, Idris) код может выглядеть годно:
do action1 obj1 a b
action2 obj1 c d
action3 obj2 eТочно так же коды возврата проверяются, и при обнаружении ошибки выполнение прерывается, и ошибка передается наверх, однако весь boilerplate спрятан в реализацию монады, при этом забыть проверить ошибку не получится.
В моем недавнем проекте на D нужно было активно работать с COM, и захотелось тоже иметь такой механизм обработки кодов возврата, чтобы с одной стороны не захламлял код в типичных сценариях, а с другой стороны не позволял забыть проверку. И получилось сделать вот так:
auto obj1 = ComPtr!SomeClass(CLSID_SomeClass); //create by GUID auto obj2 = ComPtr!ISomething(obj1); //use QueryInterface obj1.action1(a, b); obj1.action2(c, d); obj2.action3(e);
Т.е. это smart pointer, похожий на знакомый С++ программистам CComPtr, который так же как там занимается COM-овким делом подсчета ссылок (вызывает AddRef() / Release() когда следует), и позволяет вызывать методы того типа, на который содержит указатель, но в отличие от С++ не просто вызывает их, а заодно проверяет возвращенное значение и бросает нужное исключение в случае ошибки. Возможно это благодаря тому, что в D есть opDispatch - это как method_missing в Ruby, но полностью статически проверенный и скомпилированный. За основу я взял ComPtr из исходников VisualD и основательно по нему прошелся. Выглядит это как-то так:
struct ComPtr(Interface) { protected Interface ptr; // указатель на COM интерфейс или объект protected string name; //... тут всякие конструкторы, деструктор, копирование ... Interface raw() { return ptr; } bool opCast(T:bool)() { return ptr !is null; }
Неявное приведение смартпоинтера к указываемому типу я убрал, если нужен исходный голый указатель, надо явно писать
obj.raw. Для отладочных целей было добавлено поле name, чтобы отслеживать судьбу и ошибки конкретных именованнах объектов. А дальше идет opDispatch, который получает имя вызываемого метода и типы его аргументов в качестве compile-time параметров и сами аргументы в качестве run-time параметров:auto opDispatch(string fn, Ts...)(Ts vs) { import std.format; enum vss = unwrapPtrs!Ts; alias RetType = ReturnType!(mixin("ptr."~fn)); static if (is(RetType == HRESULT)) { HRESULT hr = mixin("ptr." ~ fn ~ "(" ~ vss ~ ")"); if (hr < 0) { string msg = format("%s%s::%s returned %X", name.length > 0 ? name ~ " calling " : "", Interface.stringof, fn, hr); throw new COMException(hr, msg); } return hr; } else return mixin("ptr." ~ fn ~ "(" ~ vss ~ ")"); } }
fn - это строковая compile-time переменная. Соответственно,
"ptr."~fn это тоже строка, а вот если засунуть ее в волшебное слово mixin, то она превращается в исходный код в этом самом месте. Это позволяет узнать тип возвращаемого значения у метода с переданным именем, и если это HRESULT, то не только вызвать его, но и проверить возвращенное значение. В сообщение об ошибке можно включить имя интерфейса, имя метода и имя самого объекта, если оно было передано при создании смартпоинтера. Но один нюанс: поскольку я не разрешил автоматически приводить ComPtr!T к T, то мы не можем передавать такие смартпоинтеры в COM-овские методы, ожидающие указатели на COM-овские интерфейсы, это разные типы. Поэтому при вызове такие обертки нужно развернуть, для этого в коде выше использована ф-я unwrapPtrs, получающая список типов аргументов, и знающая, что сами аргументы переданы в гетерогенном списке vs. Она для каждого аргумента смотрит, не является ли он таким смартпоинтером, и если да, то вместо vs[i] ставит vs[i].raw:enum isComPtr(T) = is(T == ComPtr!A, A); string unwrapPtrs(Ts...)() { import std.string : format; import std.array : join; string[] res; res.length = Ts.length; foreach(n, T; Ts) { static if (isComPtr!T) res[n] = format("vs[%d].raw", n); else res[n] = format("vs[%d]", n); } return res.join(", "); }
возвращает она строку вроде "vs[0], vs[1].raw, vs[2].raw, vs[3]", и эту строку opDispatch выше вставляет в код вызова метода.
Если нужно обработать ошибку не способом по-умолчанию, а иначе, достаточно добавить raw:
auto hr = obj1.raw.action1(a, b); // не бросает исключений ...
Если же нужно не просто выбросить ошибку наверх, а еще снабдить ее дополнительным сообщением, то выглядит это так:
obj1.action1(a, b).doing("Ensuring printer is on"); obj1.action2(c, d).doing("Checking paper is there"); obj2.action3(e).doing("Producing a spark");
Казалось бы, как может
doing сделать что-то полезное, если вызов obj1.action1 уже бросил исключение? Тут работает сочетание UFCS (universal function call syntax) и ленивости. Первое позволяет f(x,y) записывать как x.f(y), т.е. obj1.action1(a, b).doing(z) превращается в
doing( obj1.action1(a, b), z), а второе позволяет отложить вычисление ее аргумента:
auto doing(lazy HRESULT smth, string desc) { try { return smth; } catch(COMException ex) { throw new COMException(ex.hr, desc ~ ": " ~ ex.msg); } }
Т.е. вызов
obj1.action1(a, b) происходит внутри doing, обложенный try-catch'ем, чтобы добавить к ошибке нужную строку.Такое вот очередное применение статической интроспекции и метапрограмминга. Полный исходник модуля здесь.
no subject
Date: 2016-04-26 03:08 am (UTC)const GUID IID_IPersist = IPersist.iid; interface IPersist : IUnknown { static const GUID iid = { 0x0000010c,0x0000,0x0000,[ 0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x46 ] }; HRESULT GetClassID(CLSID *pClassID); }Благодаря этому смартпоинтеру достаточно только знать тип запрашиваемого интерфейса, а егойный GUID для QueryInterface он найдет автоматически, см. исходники по ссылке в конце поста.
no subject
Date: 2016-04-26 04:01 am (UTC)Например в одном из текущих проектов Direct3D 11, от которого в коробке с Windows SDK только C++ headers.
Какое-то время назад сделал другой C++ проект, в нём не было D3D, зато был Media Foundation, такой же COM без IDL, как D3D.
no subject
Date: 2016-04-26 04:38 am (UTC)Ну и плюсовые h-файлы в D превратить тривиально, они очень похожи. Вот так выглядел результат конверсии нужных мне интерфейсов из DirectShow:
https://gist.github.com/thedeemon/46748f91afdbcf339f55da9b355a6b56