По следам опыта создания DLL’ек.

Ну, что может быть сложного в написании DLL? Ну пишем код, помечаем функции которые экспортируются, компилируем… вуаля… готово.

Ну, это естественно на бумаге, а в жизни, как оказалось множество оврагов. Очень быстро и поверхностно (все пишется с точки зрения C/C++, но на самом деле актуально фактически для всего из чего можно собрать DLL)

— Ну начнем с простого факта. В DLLMain нельзя делать много из того, что можно делать в многих других местах.
а) Вызывать LoadLibrary, FreeLibrary
б) Работать с Registry
в) Работать с потоками и процессами. (Создавать потоки можно, но ждать их нельзя)
г) Использовать API, которые предоставляются не Kernel32.dll
д) И само собой нельзя делать все то, что использует вышеперечисленные вещи. Например нельзя использовать COM

— Увы, факт ограничений в DLL написал маленькими буквами где-то на 5 странице документации. Еще хуже то, что зачастую все будет работать нормально, до тех пор пока вам какой-нибудь заказчик не пришлет баг, которые повторяется в 30% случаев, когда луна находится в перегелии и не зная этих ограничений можно долго и счастливо все это дебажить.

— Ладно. С ограничениями разобрались. Если нам что-то надо будет сделать, мы создадим потом, который сделает эти операции или сделаем глобальную переменную объекта, в конструкторе которого сделаем все что нам надо. Правильно? Бззззз… Ответ не правильный.
а) Если мы создаем поток и не ждем его (так как ждать нельзя), мы не можем быть уверенными что DllMain уже закончится, так что мы просто создали race condition и проблемы стали еще более тяжело воспроизводимы
б) Глобальные переменные и статические члены классов инициализируются и деинициализируются внутри DllMain. Хотя это с ходу не видно, но если полезть в CRT, то это станет понятно. Таким образом, описанные ограничения касаются также конструкторов и деструкторов.

— Итого, имеем, чтобы по честному сделать какую-ту серьезную инициализацию, то нужно иметь функцию в нашей DLL которую клиент вызовет и там мы сделаем все сложные действия. Аналогично, кстати для деинициализации.

— Особенно все прикольно становится, когда мы пишем DLL которую будет впихивать в чужие процессы с помощью SetWindowHookEx или просто plugin’ы к какой-то системе. В обоих случаях, интерфейс может быть заранее определен и там нету функций инициализации и деинициализации. Соотвественно, не ясно что делать.

— Ok. Поздняя инициализация нам поможет насчет инициализации. То есть когда-то кто-то вызовет первый из интерфейсов, мы сделаем все сложные действия. А что делать с деинициализацией? В отличии поздней инициализации, ранней деинициализации не существует то? На это увы, общего ответа я не нашел и в каждом случае, нужно искать свое решение.

— Следующая особенность состоит в том, что все потоки созданные в DLL на самом деле принадлежат процессу. То есть если вдруг DLL будет выгружена из памяти (потому что вызывающий процесс сделал FreeLibrary и DLL counter стал равным 0), то внезапно адресное пространство где была DLL будет высвобождено. А поток останется __ЖИВ__. То есть он попытается выполнить свою следующую операцию, для этого попытается считать следующую команду из адресного пространства где была DLL и мгновенно закрешится. Скажите прекрасно?

— Ok. То есть нам надо остановиться все потоки, которые мы создали в DLL перед тем, как она будет выгружена и это при том, что мы еще даже точно не знаем, как нам запустить какой-то код перед выгрузкой, причем в момент, где нет ограничений. А да, и я еще молчу о том, что написать нормальный thread manager который обрабатывает достаточно большое количество разных ситуаций не вызывая dead lock’ов и race condition, задача в целом не тривиальная.

— Так… Что там у нас дальше. Ага. Вам нужно использовать COM? Само собой вы привыкли к CoInitialize(Ex) и побежали. Только вот, есть одна проблема. Если вы это делаете в DLL то вы исполняетесь в чужом потоке и у потока уже может быть инициализирован COM, причем не в том режиме (STA vs MTA) в котором вам нужно. Что делать? Создавать отдельный поток в котором выполнять все COM действия.

— Единственное пожалуй из положительного. Многое из того, что я писал относится к выгрузке DLL системой. Есть простой трюк, как удостовериться, что система никогда не выгрузит ваш DLL (исключая закрытие программы). Все что вам нужно сделать в DLL это вызывать GetModuleHandle(Ex). Это добавит 1 к DLL counter’у и можно не беспокоиться, что counter когда-либо вернется к нулевому значению.

— А еще во время выхода из процесса, система убивает все потоки, причем поток мог быть посередине модифицирования каких-нибудь данных (внутри критической секции). Еще приятней то, что он мог быть внутри функции работы с heap и таким образом heap будет в inconsistent state. И поэтому лучше не делать ничего хитрого (а по возможность вообще ничего) в DLLMain DLL_PROCESS_DETACH и в деструкторах глобальных объектов (взято отсюда: http://blogs.msdn.com/b/oldnewthing/archive/2007/05/03/2383346.aspx

Вроде все, хотя может что-то еще вспомню.

8 комментариев to “По следам опыта создания DLL’ек.”

  1. ctype:

    мне очень знакомо все, что вы описали, я «этим» долго жил …
    эта одна из причин, почему я свалил с виндовс — очень тяжело для человека, настроенного на создание «результата», вместо «результата» барахтаться в багах, в ситуации, когда эти баги и не баги а «фичи» и не твои а чужие

    • Victor Ronin:

      Как я понимаю большинство того, что я написал будет также применимо и для Shared Objects в Linux. Там тоже есть ограничения на то, что можно вызывать в функциях инициализации и конструкторах глобальных объектов. Это ограничение выросло из того, как операционные системы подгружают зависимые модули.

      Аналогично, потоки принадлежат процессам, а не DLL.

      Пожалуй единственно, что специфично для Windows — это COM и SetWindowHookEx. Правда вот COM я глотнул вдоволь и он таки мне действительно не нравиться.

      • ctype:

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

        • Victor Ronin:

          С этим я полностью согласен. Действительно на Windows разработчик не может поглядеть или исправить сорцы.

          Правда, если говорить честно, то даже на линукса, при наличии такой возможности не так много людей пользуются этой возможностью.

          • ctype:

            ну с возможностями всегда так.
            как-то было исследование по офисам, выяснилось, что обычный пользователь использует только 15% возможностей, но для каждого пользователя в эти 15% входят разные возможности
            примерно так же и осями … все-таки приятнее иметь возможность реализовать любую блажь (пусть и сложно и дорого, но все же) чем не иметь подобной возможности вообще

      • ctype:

        ну и да, это лично мое имхо. я не претендую на «знание»….
        я тут недавно нашел для себя vim — вот уже полгода прошло, а я в восторге