- производитель какого-либо устройства предоставил вам DLL для коммуникации
- требуется использовать WinAPI, например для работы с окнами
- ускорение критичных участков (LabVIEW довольно медленна сама по себе, как правило переписывание части кода на Си позволяет ускорить выполнение в несколько раз)
Для упражнений нам помимо LabVIEW понадобится какой - либо компилятор, позволяющий скомпилировать код в DLL. Я пользуюсь компилятором CVI, также подойдёт VisualStudio (бесплатная Express в том числе). Я не буду описывать процесс создания DLL и пошаговое руководство по подключению DLL к LabVIEW, об этом вы можете прочитать на сайте NI, например в статье Using Existing C Code or a DLL in LabVIEW.
Итак, начнём. Прежде всего надо заметить, что существуют два типа связи сторонних DLL с LabVIEW - статическая линковка и динамический вызов. В первом случае вы явно указываете имя и путь DLL в настройках. Во втором случае вы указываете путь к DLL на блок-диаграмме:
В основном используется статическая связь. Однако вы не сможете запустить VI, если требуемая библиотека будет отсутствовать. В подобных случаях можно подгружать библиотеки динамически, при этом на загрузку библиотеки и поиск соответствующих функций потребуется некоторое время — вот маленькое исследование на эту тему Call Library Function Node Calling Optimization.
А вот с чем имеет смысл досконально разобраться, так это с соглашениями о вызовах. Вы, вероятно уже заметили, что при подключении внешнего кода требуется установить флажок "Calling conventions":
А чем, собственно, отличаются "stdcall (WINAPI)" и "С"? Существует несколько соглашений о вызовах (stdcall, cdecl, thiscall, fastcall). Нас сейчас интересуют первые два (thiscall используется в С++, а fastcall — довольно редко). Наиболее часто используется cdecl (этому соглашению соответствует опция C), а stdcall в основном используется фирмой Микрософт как основное соглашение для вызовов WinAPI. Как правило по умолчанию в компиляторах включён режим cdecl.
Давайте сделаем небольшую тестовую DLL, в которой исследуем то, каким образом передаются и возвращаются параметры, а также изучим различные соглашения о вызовах. Мы можем смело скомбинировать оба соглашения в рамках одной DLL, для этого напишем следующий код:
Параметры в DLL можно передавать по значению или по ссылке. В вышеприведённом примере параметр In передаётся по значению, а параметр Out передаётся по ссылке (то есть передаётся не значение переменной, а адрес, по которому она находится). Подключение библиотеки выполняется следующим образом:
Обратите внимание на строку, где показывается прототип вызываемой функции — используйте эту строку для контроля правильности описания параметров:
Что же происходит "за кадром"? Давайте заглянем внутрь наших функций:
Вот как выглядит функция, вызываемая как stdcall:
А вот как выглядит функция, декларированная как cdecl:
Итак, как мы видим, происходит следующее:
Параметры передаются через стек. Регистр esp - это и есть указатель на стек. Через стек передаются два аргумента. Параметр In смещён на четыре байта и его значение переносится из стека в регистр eax. Использование именно eax не случайно - это именно тот регистр, через который осуществляется возврат значения функцией (return In). Второй параметр Out смещён на восемь байт и его значение (при этом мы помним, что передали адрес) заносится в регистр edx. Третьей командой мы записываем значение eax по адресу, находящемуся в edx, затем возвращаемся в исходную программу. Вот здесь мы и видим отличие двух соглашений о вызовах. При вызове cdecl параметры заносятся в стек в обратном порядке, при этом указатель стека не изменяется (кстати, это даёт возможность вызывать функции с переменным количеством параметров, но не в LabVIEW). При вызове stdcall параметры передаются в прямом порядке, так как они объявлены в декларации, при этом указатель стека смещается на необходимое количество байт. В случае stdcall команда retn 8 выполняет коррекцию стека на 8 байт при выходе, а при использовании cdecl коррекция стека в теле функции не требуется. Что произойдёт в случае, если мы перепутаем соглашения? Мы получим неверный указатель стека при завешении функции, что в любом случае приведёт к проблемам. Дело даже не в возможной утечке памяти в стеке, а ещё и в том, что в стеке находятся другие данные, и неверный указатель практически моментально (или через некоторое время) приведёт к падению программы. Об этом также можно почитать в разделе помощи: Configuring the Call Library Function Node. И вот здесь мы натыкаемся на прелюбопытнейший эффект: программа продолжает работать как ни в чём не бывало даже в том случае, если мы неверно указали тип вызова! Оказывается, LabVIEW автоматически корректирует указатель стека даже в случае нашей ошибки. После нескольких экспериментов легко выяснить, что это происходит при установке флага Error Checking Level в Default (именно это значение и стоит по умолчанию):
Таким образом мы видим, что по всей вероятности LabVIEW сохраняет значение указателя стека перед вызовом функции, а затем проверяет значение после выхода из функции и выполняет автоматическую коррекцию в случае, если это необходимо (поскольку прототип вызываемой функции известен, а, следовательно известно и ожидаемое значение указателя стека после вызова). Заметьте, что никаких сообщений об ошибках при этом не выводится.
Что произойдет в случае выбора других опций? Это легко выяснить:
Disabled - приводит к немедленному падению LabVIEW, что и следовало ожидать
Maximum - приводит к ошибке 1517:
Сообщение об ошибке в случае неверного типа функции документировано, об этом можно почитать в статье Call Library Function Dialog Box. Таким образом следует обращать внимание на правильность указания соглашения о вызове. В случае неверного указания типа ошибку можно заметить не сразу, а например при изменении опции контроля ошибок.
Ещё один вопрос, на который мы можем ответить — что произойдёт в том случае, если мы укажем, например, то, что фукнция возвращает значение, хотя на самом деле тип функции void, либо наоборот. В данном случае фатальных последствий не будет, так как возвращаемое значение просто записывается в регистр eax, так что в одном случае мы просто получим значение этого регистра, а в другом случае просто проигнорируем его. Утечек памяти и исключений не возникнет (пытливый читатель может разобраться с тем, что происходит при возврате восьмибайтовых типов или строки самостоятельно).
Ещё одна из самых типичных ошибок — это неверное указание типа передаваемых параметров. В вышеприведённом примере наша функция имеет два параметра. Один из них (тот, который Out) передаётся по ссылке, и мы должны указать, что передаётся адрес переменной. Давайте намеренно совершим ошибку и передадим нулевое значение вместо адреса:
Запись по нулевому адресу немедленно вызывает исключение, а вот внешнее проявление опять-таки зависит от опции Error Checking. В случае Maximum или Default результатом будет ошибка 1097:
В случае, если мы выключим обработку ошибок вообще, то LabVIEW "упадёт". Таким образом, включение обработчика ошибок приводит также и к включению внутреннего обработчика исключений (примерно также, как это делается в C++ секциями try... catch).
Побаловавшись с простыми типами, можно перейти к массивам. В простейшем случае мы просто передаём в функцию указатель на массив. Давайте попробуем сделать простое поэлементное копирование массивов:
Здесь важно понимать то, что функция понятия не имеет о размере передаваемых массивов, так что количество копируемых элементов надо явно указать как параметр и передавать извне. Кроме того надо обратить внимание на то, что память под массив надо резервировать снаружи, в LabVIEW:
Типичная ошибка, которую можно совершить — выход за пределы массива. Как правило это приводит к исключению, либо нарушению работоспособности программы. Здесь снова может помочь включение режима отлова ошибок на максимум - в этом случае LabVIEW сообщить вам то, что вы выскочили за пределы массива (впрочем это не всегда спасает её от падения). Возникает логичный вопрос — а нельзя ли передать одновременно с массивом и количество элементов, а также зарезервировать память внутри DLL? Это сделать можно, но перед рассмотрением этого вопроса имеет смысл поэкспериментировать с простыми структурами, так как у LabVIEW есть одна небольшая тонкость.
Сделаем следующий пример:
Структурам в Си отвечает кластер в LabVIEW, но типичнейшая ошибка, которую как правило совершает начинающий программист, выглядит примерно вот так:

В лучшем случае результат далёк от ожидаемого, а в худшем код вызывает исключение.
Как же правильно передать структуру (читай - кластер) в DLL и обратно? Для этого нужно разобраться с тем, как хранятся данные в кластерах LabVIEW и в структурах Си. Основной документ, с которого имеет смысл начать — How LabVIEW Stores Data in Memory.
При передаче кластера в DLL мы фактически передаём указатель на адрес памяти, в котором хранится наш кластер. DLL принимает этот адрес и обращается к элементам структуры. Очевидно то, что реальное размешение кластера в памяти должно с точностью до байта отвечать ожидаемому расположению элементов в структуре.
Прежде всего надо заметить, что массивы, строки и пути не хранятся непосредственно в кластере LabVIEW. Вместо них хранятся адреса (причём не на непосредственно данные, а на заголовки). Таким образом в приведённом примере передача массива или строки в составе кластера смысла не имеет. Поскольку массив в нашем примере хранится непосредственно в структуре, то и передавать его надо поэлементно. Здесь нас подстерегает ещё одна опасность. Каков размер структуры в Си (тот, что возвращает sizeof(TD_MyStruct))? Если вы думаете, что 13 байт (int + char + 2 x int = 4 + 1 + 2 x 4), то вы ошибётесь. Дело в том, что между членами char и int "вставлено" три дополнительных байта, которых вы не видите. Это сделано для исключения пенальти при обращении по адресам, невыровненным на границу 32х бит. Примеры выравнивания в структурах можно посмотреть в msdn: Structure Alignment Examples. По умолчанию установлено выравнивание на границу 4 байт, так что истинный размер структуры в нашем примере не 13, а 16 байт. А вот в LabVIEW элементы кластеров хранятся без выравнивания. Чтобы решить проблему выравнивания, существуют два пути - либо вставить недостающие "padding" элементы в кластер (это может быть использовано в том случае, если DLL не может быть перекомпилирована):
либо явно указать компилятору что наша структура не требует выравнивания (это достигается при помощи #pragma pack(1) непосредственно перед объявлением структуры). Обратите внимание, что #pragma pack(1) будет распространяться на все структуры до момента пока не встретится следующая #pragma pack.
В заключение - маленький совет. Поскольку порядок следования элементов в кластере весьма важен, то для того, чтобы не проверять каждый раз порядок через меню "Reorder Elements in Cluster" просто установите опцию Arrange Vertically (доступно в контекстном меню) — в этом случае расположение элементов в кластере всегда будет отражать реальный порядок следования.
Вот собственно и всё, на что стоит обратить внимание при подключении стороннего кода из DLL в LabVIEW:
- соглашение о вызовах (stdcall или cdecl)
- передача параметров по ссылке или по значению
- выравнивание в структурах и порядок следования элементов.
За кадром пока что остались опции управления потоками исполнения (UI или любой поток), функции управления менеджером памяти LabVIEW из стороннего кода и функции обратного вызова при определённых событиях, но об этом как-нибудь в другой раз...
Комментариев нет:
Отправить комментарий