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-25 01:40 pm (UTC)Что касается Go, я был бы доволен как слон если бы был такой сахар:
который бы вызывал скобки при ненулевом последнем значении, имеющем тип error. Все остальное, включая исключения, на мой взгляд хуже.
no subject
Date: 2016-04-25 01:49 pm (UTC)Проблема с этим последним кодом в том, что все задекларированные через := переменные попадают в область видимости блока if-else и невидны снаружи, а это частенько бывает неудобно.
no subject
Date: 2016-04-25 01:57 pm (UTC)no subject
Date: 2016-04-25 01:59 pm (UTC)no subject
Date: 2016-04-25 03:54 pm (UTC)no subject
Date: 2016-04-25 05:00 pm (UTC)no subject
Date: 2016-04-25 06:36 pm (UTC)no subject
Date: 2016-04-26 02:18 am (UTC)static if (!__traits(compiles, someShit)) pragma(msg, format("my error %s with details %d and %s", x, y, z))no subject
Date: 2016-04-26 04:49 am (UTC)no subject
Date: 2016-04-25 02:00 pm (UTC)no subject
Date: 2016-04-25 04:36 pm (UTC)1) инверсия порядка выполнения
obj.foo() - энергичный вызов
obj.foo().doing("blabla") - сперва макроколдунство, потом вызов.
2) зарезали возможности цепочек вызовов
obj.foo().bar() - последовательность вызовов
obj.foo().doing("blabla") - выглядит так же, а смысл радикально другой
А что касается извечного чернышевского вопроса "что делать с HRESULT", то ответ оказывается богаче предустановленных вариантов.
Поэтому обёртывание во внешнюю функцию (или макрос) - наиболее гибкий подход, ибо макросов на разные случаи можно наделать сколько угодно, а подмешивать разные doing в свой надкласс, а тем более, писать разные надклассы - неблагодарное занятие.
Навскидку, HRESULT может быть
- SUCCEEDED / FAILED
- S_OK / всё остальное
- S_OK / S_FALSE / всё остальное
- S_OK / FAILED нестрашное / FAILED страшное
а механизмы выбрасывания
- return, если контекст - void-функция
- return hr, если контекст - HRESULT-функция
- throw _com_error(hr) - если выше по стеку - try-catch именно _com_error
в общем, на ровном месте получаем комбинаторный взрыв.
Макросами его покрыть - два байта переслать.
А для установления контекста возникновения - старые добрые __FILE__, __LINE__ решают.
no subject
Date: 2016-04-26 03:21 am (UTC)С doing это хак, конечно, и его можно ругать. Но в рамках небольшого проекта вполне ок получилось.
Комбинаторный взрыв получается, когда нет единообразности. В теории там вариантов много, а на практике не очень. Есть самый частый случай, он тут покрыт самым коротким и простым кодом, и бывают изредка девиации, там пишу
auto hr = obj.raw.method(a,b);
и с этим hr уже можно решить что делать.
А вот что делать наверху, если прилетело исключение, там в моем случае 3 варианта всего было, и они были описаны аннотациями и тоже автоматически обработаны.
no subject
Date: 2016-04-26 01:54 pm (UTC)try!(call_some_fun(obj1, obj2, obj3), "must work")
vs
obj0.call_some_fun(obj1, obj2, obj3).doing("must work");
Так-то понятно, что конкретную задачу решил, работает, - не трогать, а похвалить себя (иначе никто не похвалит).
Пользуясь случаем, - спасибо за демонстрацию макровозможностей D. Чего только в мире не бывает!
А взрыв - это, к сожалению, на практики. Не сталкивался бы с ним, не упомянул бы.
no subject
Date: 2016-04-25 04:41 pm (UTC)no subject
Date: 2016-04-25 06:09 pm (UTC)no subject
Date: 2016-04-25 08:55 pm (UTC)Практически, я хорошо умею С++ с макросами, недостаточно профита, шоб переходить с C++ на D.
Ещё интересно, откуда берётся type info от COM интерфейсов?
Умеет ли импортировать C++-ные заголовки из всяких windows SDK?
Умеет ли импортировать COM type libraries, как это делает #import "progid..." в плюсах?
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
no subject
Date: 2016-04-26 09:53 pm (UTC)no subject
Date: 2016-04-27 02:18 am (UTC)На практике VisualD сейчас находит такие вызовы по имени: если я прошу показать определение method в выражении obj.method, где obj - такой вот смартпоинтер, то если имя уникальное, показывает правильно, а если не уникальное, то предлагает выбрать из найденных вариантов.
no subject
Date: 2016-04-26 10:05 pm (UTC)no subject
Date: 2016-04-26 10:26 pm (UTC)Допустим есть некая функция
def func(text):
print(text)
или класс
class SomeClass:
def method(text):
print(text)
Так в Пайтоне я смогу
ch = ChainWrapper(...разные там параметры...)
ch().func('Hello')
sc = SomeClass()
ch().sc.method('World')
Ну и оно атоматом индивидуально выполняет последующие функции или индивидуально же пропускает их (если требуемые блоки кода / фукнции небыли заранее выполнены).
А то до этого, чисто для функций у меня было что-то вроде (если предположить что func() уже обернута в декоратор):
func('Hello', chain_parameter_1=some_value, chain_parameter_2=some_value_2, ... etc.)
Так что еще раз спасибо за наводку! :)
no subject
Date: 2016-04-26 10:31 pm (UTC)no subject
Date: 2016-04-28 03:26 pm (UTC)Что-то уже 3 раза делал заход написать что-то объёмное, но упирался в отсутствие системы сборки (cmake там) и IDE.
Последний раз через неделю мучений с запуском IDE (ddt, monod, dlangide, Intelllij) ничего не полетело. Code::Blocks, Coedit, dlangide какие-то недоделаные или это стиль такой.
Плюнул, когда прикрутил плагин к qtcreator и он упал с segfault.
вообще пишу на qtcreator/cmake/c++.
Почему никак не могут с cmake прикрутить к D официально? dub это всё-таки немного про скачать либу. (я так и не смог сделать проект, чтобы можно было несколько бинарей собрать за один dub build)
no subject
Date: 2016-04-28 04:23 pm (UTC)С виндой попроще, я вот вчерась отчетик запилил:
http://forum.dlang.org/thread/rivvjmrqmhaacfwxwish@forum.dlang.org
Про нескольно бинарей в dub недавно на форуме проскакивал вопрос, вроде чего-то предлагали в ответ. На cmake как-то не видно большого спроса у сообщества.
no subject
Date: 2016-04-28 06:47 pm (UTC)да, ощущения именно такие: каждый пилит своё и нет законченности. видимо, потому что в отличии от С++, в D есть continuations.
no subject
Date: 2016-04-28 06:44 pm (UTC)можно использовать в теле как foo и как foo()
(https://dlang.org/lazy-evaluation.html тут предлагают foo() )
чекнул - действительно так, что за мутки?
no subject
Date: 2016-04-29 02:06 am (UTC)int foo();можно использовать как простоfoo.Разница между foo и foo() ускользающе мала.
А если обычную ф-ю
void foo(int x);как@propertyпометить, то можно в нее что-то передать какfoo = 3;no subject
Date: 2016-04-29 07:42 am (UTC)Не знаю, как-то это всё не очень интуитивно понятно.
Это как с++ когда скрестили старые типы с новыми, оказалось, что
Одни нужно создавать так
B b(1,2,4); //
а другие так
A a;
при этом если сделать всё одинаково будем опа.
A a(); // вообще является объявлением функции.
no subject
Date: 2016-04-29 09:54 am (UTC)Но в целом логично: если работает для свойств классов и структур, почему бы не быть свойствам у модулей.
no subject
Date: 2016-04-29 08:08 am (UTC)no subject
Date: 2016-04-29 09:52 am (UTC)Скажем, был код, что-то пишущий в глобальную переменную. Взяли сделали ее свойством, теперь при ее изменении можно сразу как-то реагировать.