thedeemon: (office)
[personal profile] thedeemon
Как вы, вероятно, знаете, в мире Windows до сих пор COM живее всех живых, и поскольку его должно быть можно использовать и из Си, работа с ошибками там построена на базе кодов возврата: функции обычно возвращают HRESULT, целое число, которое 0 если все ок, отрицательно при ошибке и положительно, если это как бы не ошибка, но и не совсем полный успех. Ошибки надлежит не игнорировать и не пропускать, как-то обрабатывать, однако очень часто вся обработка сводится к тому, чтобы прекратить выполнение текущей ф-ии и передать эту ошибку наверх. В языках вроде Go это превращается в код вида

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'ем, чтобы добавить к ошибке нужную строку.

Такое вот очередное применение статической интроспекции и метапрограмминга. Полный исходник модуля здесь.

Date: 2016-04-25 01:40 pm (UTC)
From: [identity profile] sober-space.livejournal.com
Я бы этим не пользовался, т.к. ничего не понятно что происходит. Просто какой-то вызов, и неочевидно с первого взгляда на код, что от него можно ждать.

Что касается Go, я был бы доволен как слон если бы был такой сахар:

err := obj1.Action1(a, b) { return err }
err := obj1.Action2(c, d) { DO SOMETHING }
err := obj2.Action3(e) { DO SOMETHING; return err }


который бы вызывал скобки при ненулевом последнем значении, имеющем тип error. Все остальное, включая исключения, на мой взгляд хуже.

Date: 2016-04-25 01:49 pm (UTC)
From: [identity profile] sober-space.livejournal.com
Но там все равно был бы небольшой выигрыш в сравнении со стандартным

if err := obj1.Action1(a, b); err != nil { return err }


Проблема с этим последним кодом в том, что все задекларированные через := переменные попадают в область видимости блока if-else и невидны снаружи, а это частенько бывает неудобно.

Edited Date: 2016-04-25 01:52 pm (UTC)

Date: 2016-04-25 01:57 pm (UTC)
wizzard: (Default)
From: [personal profile] wizzard
симпатично

Date: 2016-04-25 01:59 pm (UTC)
wizzard: (Default)
From: [personal profile] wizzard
"статически проверенный" - а откуда оно берет список методов интерфейса?

Date: 2016-04-25 03:54 pm (UTC)
From: [identity profile] thedeemon.livejournal.com
Компилятор же как в С++ все конкретные использования шаблонов инстанцирует, т.е. в каждом конкретном случае знает что за тип там в параметре и какие у него есть методы, а каких нет. Если такого метода нет, то данный вызов opDispatch не скомпилится.

Date: 2016-04-25 05:00 pm (UTC)
From: [identity profile] binf.livejournal.com
в с++ в этом случае будет сообщение об ошибке на два экрана

Date: 2016-04-25 06:36 pm (UTC)
From: [identity profile] binf.livejournal.com
а кстати, можно ли на D вставить в теплэйт статическую проверку чтобы получить осмысленное сообщение об ошибке в случае отсутствия нужного метода?

Date: 2016-04-26 02:18 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Да, можно. Например в таком духе:
static if (!__traits(compiles, someShit))
  pragma(msg, format("my error %s with details %d and %s", x, y, z))

Date: 2016-04-26 04:49 am (UTC)
From: [identity profile] binf.livejournal.com
О, в таком случае типизированные дженерики не нужны, если это применять

Date: 2016-04-25 02:00 pm (UTC)
From: [identity profile] justy-tylor.livejournal.com
Делал такое на разных языках, и даже на сишечке-с-макросами (C++ не дозволялся в той части телефонной прошивки), но обычно с нормализацией. Т.е. контекст (doing) указывался не для действия, а для произвольного блока действий. Ну и такими же контекстами стратегии "как ловить", вместо ручных указаний try/catch.

Date: 2016-04-25 04:36 pm (UTC)
From: [identity profile] kodt-rsdn.livejournal.com
Мутный подход, однако.

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__ решают.

Date: 2016-04-26 03:21 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Макросами безобразно же.

С doing это хак, конечно, и его можно ругать. Но в рамках небольшого проекта вполне ок получилось.

Комбинаторный взрыв получается, когда нет единообразности. В теории там вариантов много, а на практике не очень. Есть самый частый случай, он тут покрыт самым коротким и простым кодом, и бывают изредка девиации, там пишу
auto hr = obj.raw.method(a,b);
и с этим hr уже можно решить что делать.

А вот что делать наверху, если прилетело исключение, там в моем случае 3 варианта всего было, и они были описаны аннотациями и тоже автоматически обработаны.

Date: 2016-04-26 01:54 pm (UTC)
From: [identity profile] kodt-rsdn.livejournal.com
Макросы-функции хороши тем, что применимы к любым выражениям, тогда как макросы-методы - только к методам пропатченного класса.

try!(call_some_fun(obj1, obj2, obj3), "must work")
vs
obj0.call_some_fun(obj1, obj2, obj3).doing("must work");

Так-то понятно, что конкретную задачу решил, работает, - не трогать, а похвалить себя (иначе никто не похвалит).

Пользуясь случаем, - спасибо за демонстрацию макровозможностей D. Чего только в мире не бывает!

А взрыв - это, к сожалению, на практики. Не сталкивался бы с ним, не упомянул бы.

Date: 2016-04-25 04:41 pm (UTC)
From: [identity profile] binf.livejournal.com
Очень сложно. По мне так в Rust семантика обработки ошибок на основе Result<'T,'E> и try!(...) более гуманная.
Edited Date: 2016-04-25 04:48 pm (UTC)

Date: 2016-04-25 06:09 pm (UTC)
From: [identity profile] juan-gandhi.livejournal.com
Горячо приветствую! Вот же ж монада впендюрена как надо. Я уже как бы возмущаюсь своими решениями в Скале...

Date: 2016-04-25 08:55 pm (UTC)
From: [identity profile] soonts.livejournal.com
В принципе круто.
Практически, я хорошо умею С++ с макросами, недостаточно профита, шоб переходить с C++ на D.

Ещё интересно, откуда берётся type info от COM интерфейсов?
Умеет ли импортировать C++-ные заголовки из всяких windows SDK?
Умеет ли импортировать COM type libraries, как это делает #import "progid..." в плюсах?

Date: 2016-04-26 03:08 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Есть утилита, переводящая IDL файлы из SDK в исходники на D. Причем она сразу делает одну хорошую вещь: делает IID интерфейса статическим членом типа, а привычный IID_ISomething ссылается туда.

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 он найдет автоматически, см. исходники по ссылке в конце поста.

Date: 2016-04-26 04:01 am (UTC)
From: [identity profile] soonts.livejournal.com
А шо делать, если у меня нет IDL?

Например в одном из текущих проектов Direct3D 11, от которого в коробке с Windows SDK только C++ headers.
Какое-то время назад сделал другой C++ проект, в нём не было D3D, зато был Media Foundation, такой же COM без IDL, как D3D.

Date: 2016-04-26 04:38 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Есть еще конвертер из TLB в IDL.

Ну и плюсовые h-файлы в D превратить тривиально, они очень похожи. Вот так выглядел результат конверсии нужных мне интерфейсов из DirectShow:
https://gist.github.com/thedeemon/46748f91afdbcf339f55da9b355a6b56

Date: 2016-04-26 09:53 pm (UTC)
From: [personal profile] zaharchenko
А IDE или статический какой-нибудь анализатор сможет тогда понять всё это и найти вызовов action1 у CLSID_SomeClass? В смысле find usages работать будет?

Date: 2016-04-27 02:18 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Теоретически, сможет, компилятор же понимает статически где что вызывается.
На практике VisualD сейчас находит такие вызовы по имени: если я прошу показать определение method в выражении obj.method, где obj - такой вот смартпоинтер, то если имя уникальное, показывает правильно, а если не уникальное, то предлагает выбрать из найденных вариантов.

Date: 2016-04-26 10:05 pm (UTC)
From: [identity profile] fi_mihej.livejournal.com
За "obj1.action1(a, b);" кстати большое спасибо: дало пример дизайна (который я как-то видел в JS, но про который совсем забыл, т.к. JS не использую вообще) краткой и более удобной обертки для эдакой лямбды Питоновской сделанной для сложных chain-вычислений. А то для блоков кода - и with-нотация хорошо идет, а вот так чтобы функцию немногословно обернуть - даже декоратора недостаточно (доп-параметры для него - много места съедают, и гадко смотрятся) :)

Date: 2016-04-26 10:26 pm (UTC)
From: [identity profile] fi_mihej.livejournal.com
Думаю, лучше все же поясню что имею в виду, а то сумбурно написал, и единственное что можно понять - это то что я благодарен. :)

Допустим есть некая функция

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.)

Так что еще раз спасибо за наводку! :)
Edited Date: 2016-04-26 10:28 pm (UTC)

Date: 2016-04-26 10:31 pm (UTC)
From: [identity profile] fi_mihej.livejournal.com
Хотя нет: с методами классов я пожалуй погорячился: так можно только с функторами будет сделать. А для методов - оставить декоратор. Но тож ничего! :)

Date: 2016-04-28 03:26 pm (UTC)
From: [identity profile] notaden.livejournal.com
А не появилась вменяемая IDE под linux? я уж было согласен был на простой редактор с подсветкой и хоть каким-то авто-дополнениями (dcd и т.п.), главное, чтобы было как-то доделано всё (ну и с какой-нибудь системой сборки интергрировалось и по двойному клику на ошибку - прыгало на нужную строку).

Что-то уже 3 раза делал заход написать что-то объёмное, но упирался в отсутствие системы сборки (cmake там) и IDE.

Последний раз через неделю мучений с запуском IDE (ddt, monod, dlangide, Intelllij) ничего не полетело. Code::Blocks, Coedit, dlangide какие-то недоделаные или это стиль такой.

Плюнул, когда прикрутил плагин к qtcreator и он упал с segfault.


вообще пишу на qtcreator/cmake/c++.

Почему никак не могут с cmake прикрутить к D официально? dub это всё-таки немного про скачать либу. (я так и не смог сделать проект, чтобы можно было несколько бинарей собрать за один dub build)

Date: 2016-04-28 04:23 pm (UTC)
From: [identity profile] thedeemon.livejournal.com
Не, про линукс не скажу, там я просто vim + YouCompleteMe использовал, даже без DCD. Как я слышал, MonoD должна быть самой продвинутой там. На хорошо доделанную IDE банально людей не хватает, энтузиасты каждый свое клепает, да ни один до конца не доводит.

С виндой попроще, я вот вчерась отчетик запилил:
http://forum.dlang.org/thread/rivvjmrqmhaacfwxwish@forum.dlang.org

Про нескольно бинарей в dub недавно на форуме проскакивал вопрос, вроде чего-то предлагали в ответ. На cmake как-то не видно большого спроса у сообщества.

Date: 2016-04-28 06:47 pm (UTC)
From: [identity profile] notaden.livejournal.com
MonoD не осилил вроде единственную из списка. Попробую.
да, ощущения именно такие: каждый пилит своё и нет законченности. видимо, потому что в отличии от С++, в D есть continuations.

Date: 2016-04-28 06:44 pm (UTC)
From: [identity profile] notaden.livejournal.com
кстати, " lazy int foo "
можно использовать в теле как foo и как foo()
(https://dlang.org/lazy-evaluation.html тут предлагают foo() )
чекнул - действительно так, что за мутки?
Edited Date: 2016-04-28 06:47 pm (UTC)

Date: 2016-04-29 02:06 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Так и ф-ю int foo(); можно использовать как просто foo.
Разница между foo и foo() ускользающе мала.
А если обычную ф-ю void foo(int x); как @property пометить, то можно в нее что-то передать как foo = 3;

Date: 2016-04-29 07:42 am (UTC)
From: [identity profile] notaden.livejournal.com
Не поверил. проверил. Действительно так :)

Не знаю, как-то это всё не очень интуитивно понятно.


Это как с++ когда скрестили старые типы с новыми, оказалось, что

Одни нужно создавать так
B b(1,2,4); //

а другие так
A a;

при этом если сделать всё одинаково будем опа.
A a(); // вообще является объявлением функции.

Date: 2016-04-29 09:54 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Непривычно и неинтуитивно, да.
Но в целом логично: если работает для свойств классов и структур, почему бы не быть свойствам у модулей.

Date: 2016-04-29 08:08 am (UTC)
From: [personal profile] zaharchenko
А зачем вообще может потребоваться вот так foo = 3 ф-ии вызывать?

Date: 2016-04-29 09:52 am (UTC)
From: [identity profile] thedeemon.livejournal.com
Да так же как со свойствами у классов.
Скажем, был код, что-то пишущий в глобальную переменную. Взяли сделали ее свойством, теперь при ее изменении можно сразу как-то реагировать.

Profile

thedeemon: (Default)
Dmitry Popov

December 2025

S M T W T F S
 12 3456
789101112 13
14151617181920
21222324252627
28293031   

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jan. 6th, 2026 11:52 am
Powered by Dreamwidth Studios