Вызов разделяемых библиотек из Similink

от автора

Привет, хабр!
Представляю вашему вниманию перевод статьи моего коллеги Михаила, посвященной методам вызова разделяемых библиотек в Simulink. Зачем она была создана вообще? Дело в том, что у многих компаний уже есть множество легаси-моделей, которые хотелось бы переиспользовать и нам часто задают вопросы «А как мне легаси интегрировать в Simulink? А если мое легаси в виде DLL?» Поэтому-то и была написана оригинальная статья.
Под катом рассматривается несколько способов по вызову разделяемых библиотек в Simulink. Исходники и модели доступны на File Exchange
Все изменения в код внесены с разрешения Михаила и сделаны, что бы не перегружать статью.


В данной статье показано, как использовать функции из разделяемых библиотек в моделях Simulink.
Допустим, нам надо встроить в Simulink библиотеку exlib. Ее код содержится в исходниках exlib.c и exlib.h. Это очень простая библиотека, содержащая три функции:
void exlib_init(void) – открывает файл на запись.
void exlib_print(float data) – записывает данные в файл.
void exlib_term(void) – закрывает файл.

Сборка библиотеки

Для начала скомпилируем библиотеку.
Этот шаг не нужен, если библиотека предварительно скомпилирована и поставляется в виде двоичного файла. В этом случае сразу можно перейти к шагу 3, но надо помнить, что для работы с библиотекой нужно получить файл .dll (или .so для Linux) и соответствующий файл заголовка (.h) у поставщика библиотеки. Для Windows также понадобится файл .lib, который представляет собой не статическую библиотеку, а так называемую библиотеку импорта. Эта библиотека импорта потребуется для неявной линковки.
Сначала, будем использовать команду MATLAB mex, которая вызывает текущий активный компилятор хоста для компиляции общей библиотеки (exlib.dll в Windows и exlib.so в Linux). Важно убедиться, что сначала была выполнена команда

mex -setup 

для выбора поддерживаемого компилятора.
Теперь используем mex для компиляции библиотеки:

Код для сборки библиотеки

mingw = strfind(mex.getCompilerConfigurations('C','Selected').Name,'MinGW64 Compiler'); if isunix     % GCC     mex('LDEXT=.so','LINKEXPORT=','LINKEXPORTVER=','LINKLIBS=','exlib.c'); elseif mingw     % MinGW builds static shared library, so dll and lib files are the same     % loadlibrary uses the dll file, while legacy code tool looks for the lib file     mex('LDEXT=.lib','LINKEXPORT=','LINKEXPORTVER=','LINKLIBS=','exlib.c');     mex('LDEXT=.dll','LINKEXPORT=','LINKEXPORTVER=','LINKLIBS=','exlib.c'); else     % Visual     mex('LDEXT=.dll','LINKEXPORT=','LINKEXPORTVER=','CMDLINE300="del exlib.exp exlib.dll.manifest"','exlib.c'); end Building with 'gcc'. MEX completed successfully.

Проверка скомпилированной библиотеки

После компиляции необходимо удостоверится в том, что полученная библиотека может быть подгружена и использована в MATLAB:

Код для проверки библиотеки

% Load the library [~,~] = loadlibrary(['exlib',system_dependent('GetSharedLibExt')],'exlib.h'); % Display functions available in the library libfunctionsview('exlib'); % Initialize calllib('exlib','exlib_init'); % Step for i = 1:10     calllib('exlib','exlib_print',single(i)); end % Terminate calllib('exlib','exlib_term'); % Unload the library unloadlibrary('exlib'); % Show contents of generated file type('exlib.txt'); 0.000000 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 9.000000 10.000000 

Теперь, когда мы удостоверились, что все работает, начнем работать с Simulink!

Вызов разделяемой библиотеки с помощью S-функций

S-функции позволяют использовать C-код в Simulink. Этот подход позволяет пользователю разработать любой код на C, необходимый для загрузки библиотеки и вызова ее функций. Затем, этот код компилируется в бинарный файл, который распознается Simulink и связывается с общей библиотекой. Этот двоичный файл называется S-функцией. В Simulink есть блок для вызова этой S-функции. Существует несколько подходов для создания S-функций в Simulink.

Использование Legacy Code Tool

Первым подходом является использование Legacy Code Tool, инструмента, который помогает автоматически создавать S-функции по спецификациям, созданным с помощью MATLAB. В этом примере S-функция линкуется с библиотекой импорта (в Windows нам потребуется соответствующий файл * .lib) и происходит вызов библиотечных функций. Этот подход называется неявной линковкой.
Сначала, требуется инициализировать структуру для Legacy Code Tool

Код для инициализации структуры Legacy Code Tool

specs = legacy_code('initialize'); % Prototype for the initialization function specs.StartFcnSpec = 'exlib_init()'; % Prototype for the step function specs.OutputFcnSpec = 'exlib_print(single u1)'; % Prototype for the terminate function specs.TerminateFcnSpec = 'exlib_term()'; % Shared library to link with (.so on Linux and .dll on Windows) specs.HostLibFiles = {['exlib',strrep(system_dependent('GetSharedLibExt'),'.dll','.lib')]}; % We must supply header file when linking with shared library, otherwise % compiler might make wrong assumptions about function's prototype. specs.HeaderFiles = {'exlib.h'}; specs.SFunctionName = 'sfun_exlib'; 

Создадим S-функцию, скомпилируем и слинкуем ее:

legacy_code('generate_for_sim',specs); ### Start Compiling sfun_exlib     mex('sfun_exlib.c', '-I/tmp/simulink_shrlib_fex', '/tmp/simulink_shrlib_fex/exlib.so') Building with 'gcc'. MEX completed successfully. ### Finish Compiling sfun_exlib ### Exit 

Наконец, создадим блок Simulink:

legacy_code('slblock_generate',specs); 

Запустим симуляцию:

open_system('simlib_test'); snapnow; sim('simlib_test'); % Observe the results: type('exlib.txt'); 0.000000 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 9.000000 10.000000

Написание S-функций вручную

Второй подход заключается в самостоятельном написании собственных S-функций для загрузки разделяемой библиотеки и вызова функций из этой библиотеки. В этом подходе будет использоваться явная линковка, также называемая линковкой во время выполнения. Самым простым решением является адаптация S-функции, автоматически сгенерированной ранее Legacy Code Tool. В этом примере изменяются функции mdlStart, mdlOutputs и mdlTerminate.
Вот как эти функции выглядят после модификации:

Посмотреть код

void mdlStart(SimStruct *S) {   /*    * Load the dynamic library    */    #if defined(__GNUC__) && !defined(__MINGW32__)     void (*exlib_init_ptr)(void);     dllHandle = dlopen("./exlib.so",RTLD_LAZY);     exlib_init_ptr = dlsym(dllHandle, "exlib_init");   #else     exlib_init_type exlib_init_ptr = NULL;     dllHandle = LoadLibrary("exlib.dll");     exlib_init_ptr = (exlib_init_type)GetProcAddress(dllHandle,"exlib_init");   #endif    exlib_init_ptr(); } 
void mdlOutputs(SimStruct *S, int_T tid) {   real32_T *u1 = 0;    #if defined(__GNUC__) && !defined(__MINGW32__)     void (*exlib_print_ptr)(float);     exlib_print_ptr = dlsym(dllHandle,"exlib_print");   #else     exlib_print_type exlib_print_ptr = NULL;     exlib_print_ptr = (exlib_print_type)GetProcAddress(dllHandle,"exlib_print");   #endif    /*    * Get access to Parameter/Input/Output/DWork/size information    */   u1 = (real32_T *) ssGetInputPortSignal(S, 0);    /*    * Call the function from library    */   exlib_print_ptr( *u1); } 
void mdlTerminate(SimStruct *S) {   /*    * Unload the dynamic library    */    #if defined(__GNUC__) && !defined(__MINGW32__)     void (*exlib_term_ptr)(void);     exlib_term_ptr = dlsym(dllHandle,"exlib_term");     exlib_term_ptr();     dlclose(dllHandle);   #else     exlib_term_type exlib_term_ptr = NULL;     exlib_term_ptr = (exlib_term_type)GetProcAddress(dllHandle,"exlib_term");     exlib_term_ptr();     FreeLibrary(dllHandle);   #endif } 

Скомпилируем полученную S-функцию:

if isunix     mex('sfun_exlib_dyn.c','-ldl'); else     mex('sfun_exlib_dyn.c'); end Building with 'gcc'. MEX completed successfully.

И запустим симуляцию:

open_system('simlib_test_dyn'); sim('simlib_test_dyn'); % Observe the results: type('exlib.txt'); 0.000000 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 9.000000 10.000000

Использование S-Function Builder

Еще один подход заключается в использовании блока S-Function Builder. Это специальный блок, который может рассматриваться как нечто среднее между Legacy Code Tool и написанными вручную S-функциями с точки зрения сложности. S-Function Builder предоставляет графический интерфейс, в котором описываются характеристики вашей S-функции, и S-функция создается автоматически.
При использовании S-Function Builder существуют некоторые ограничения и проблемы с производительностью. В настоящее время это считается устаревшим подходом к созданию S-функций. Рекомендуется использовать блок C function, который появился в релизе R2020а и обсуждается позже.

Вызов разделяемой библиотеки с помощью MATLAB Function

Блок MATLAB Function позволяет использовать язык MATLAB для описания пользовательского алгоритма в Simulink.
Для вызова разделяемой библиотеки из функции MATLAB используется функция coder.ceval, которую можно использовать только в теле MATLAB Function (а не MATLAB). Для работы coder.ceval не требуется наличие MATLAB Coder.
Код для блока MATLAB Function, вызывающий разделяемую библиотеку:

Посмотреть код

function fcn(u) %#codegen  % Keep track of initialization and runtime count persistent runTimeCnt  % Generate library path on the fly (current directory in this case) coder.extrinsic('pwd','system_dependent'); libpath = coder.const(pwd); % Shared library to link with libname = coder.const(['exlib',strrep(system_dependent('GetSharedLibExt'),'.dll','.lib')]); % Add the external library. Mark it as precompiled, so it won't appear as % makefile target during code generation. coder.updateBuildInfo('addLinkObjects',libname,libpath,1000,true,true); coder.updateBuildInfo('addIncludePaths',libpath); coder.cinclude('exlib.h');  if isempty(runTimeCnt)     % Initialize     coder.ceval('exlib_init');     runTimeCnt = 0; end % Step coder.ceval('exlib_print',single(u)); runTimeCnt = runTimeCnt+1; % Terminate on the 10th step if (runTimeCnt == 11)     coder.ceval('exlib_term'); end 

Данный подход имеет один недостаток – отсутствие хорошего способа вызвать функцию завершения при окончании симуляции (по сравнению с блоком MATLAB System).
Можно определить функцию завершения для модели, поставив exlib_term (); в настройках модели в категории Simulation Target -> Custom Code -> Terminate function.

ПРИМЕЧАНИЕ. Если в настройках модели установлен параметр «Import custom code», то требуется указать все зависимости кода на панели «Simulation Target» (вместо использования coder.cinclude и coder.updateBuildInfo). Если этот параметр не установлен, то можно объединить настройки из Simulation Target, coder.cinclude и coder.updateBuildInfo.

Другой способ — поддерживать зависимости, используя coder.cinclude и coder.updateBuildInfo, и вызывать exlib_term() по условию, как продемонстрировано в примере выше.
Запустим симуляцию:

open_system('simlib_test_mlf'); sim('simlib_test_mlf'); % Observe the results: type('exlib.txt'); 0.000000 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 9.000000 10.000000

Вызов разделяемой библиотеки из Stateflow

Если требуется использовать функции разделяемой библиотеки в диаграммах Stateflow, то эти функции следует вызывать напрямую из Stateflow. В документации Stateflow есть примеры, которые показывают, как это сделать.
Вызов внешней функции в Stateflow очень прост – требуется указать имя функции в диаграмме Stateflow:

Дополнительно, необходимо настроить параметры модели, чтобы Stateflow знал, где искать эти внешние функции. В настройках Simulation Target -> Custom Code -> Libraries требуется ввести exlib.lib (или exlib.so в Linux). В Simulation Target -> Custom Code -> Header File требуется ввести #include «exlib.h». Так же важно не забыть указать функцию завершения. В Simulation Target -> Custom Code -> Terminate function необходимо указать exlib_term() ;.
Запустим симуляцию:

if isunix     set_param('simlib_test_sf','SimUserLibraries','exlib.so'); end sim('simlib_test_sf'); % Observe the results: type('exlib.txt'); 0.000000 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 9.000000 10.000000

Обратите внимание, что информация в этом разделе относится к диаграммам Stateflow, использующим язык действий C. В том случае, если диаграмма Stateflow использует язык действий MATLAB, то требуется использовать coder.ceval, как и для MATLAB Function.

Вызов разделяемой библиотеки с помощью блока MATLAB System

Блок MATLAB System позволяет использовать системные объекты в Simulink. Более подробную информацию об этом блоке можно найти в документации.
Поддержка системных объектов появилась в Simulink в релизе R2013b. Множество людей используют системные объекты, поскольку они позволяют легко определять функции инициализации, шага симуляции и завершения. Также системные объекты могут использовать вспомогательный код MATLAB для этих функций — например, для пред- и постобработки данных функции шага — и все это без написания кода на C.
Вот как выглядит системный объект, используемый в блоке MATLAB System:

Код системного объекта

classdef exlib < matlab.System     % Call exlib shared library     %     % This example shows how to call shared library from Simulink using     % MATLAB System block.     properties (Nontunable,Access=private)         libName = exlib.getLibName;         libPath = pwd;         libHeader = 'exlib.h';     end          methods (Static)         function libName = getLibName             if isunix                 libName = 'exlib.so';             else                 libName = 'exlib.lib';             end         end     end          methods (Access=protected)         function setupImpl(obj, ~)             % Initialize.             coder.updateBuildInfo('addLinkObjects',obj.libName,obj.libPath,1000,true,true);             coder.updateBuildInfo('addIncludePaths',obj.libPath);             coder.cinclude(obj.libHeader);             coder.ceval('exlib_init');         end                  function stepImpl(~, u)             % Step.             coder.ceval('exlib_print',u);         end                  function releaseImpl(~)             % Terminate.             coder.ceval('exlib_term');         end     end end

Запустим симуляцию:

open_system('simlib_test_mls'); sim('simlib_test_mls'); % Observe the results: type('exlib.txt'); 0.000000 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 9.000000 10.000000

Вызов разделяемых библиотек с помощью блока C Caller

Блок C Caller позволяет вызывать функции C напрямую из Simulink. Более подробную информацию об этом блоке можно найти в документации.
Это относительно новый подход, который впервые появился в MATLAB R2018b. Его основная цель — сделать в Simulink вызовы функций и библиотек на C черезвычайно простыми. Но у него есть ограничения, о которых вы можете прочитать в документации по этому блоку.
После того, как exlib.so/exlib.lib были добавлены в Simulation Target -> Libraries и #include «exlib.h» в Simulation Target -> Header в настройках модели, достаточно нажатия кнопки «Refresh custom code» в блоке C Caller, чтобы увидеть все функции, содержащиеся в библиотеке.
После выбора функции exlib_print, диалог спецификации портов заполняется автоматически:

И снова, требуется добавить вызовы функций exlib_init и exlib_term, в Simulation Target. Также можно добавить пару других блоков C Caller для непосредственного вызова функций инициализации и завершения. Данные блоки C Caller потребуется поместить в подсистемы Initialize Function и Terminate Function. Так же можно рассмотреть следующий пример из Stateflow: Schedule Subsystems to Execute at Specific Times
Запустим симуляцию:

open_system('simlib_test_ccaller'); if isunix     set_param('simlib_test_ccaller','SimUserLibraries','exlib.so'); end sim('simlib_test_ccaller'); % Observe the results: type('exlib.txt'); 0.000000 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 9.000000 10.000000

Вызов разделяемых библиотек с помощью блока C Function

Новейшим дополнением к интеграции внешнего кода в Simulink является блок C Function. Он появился в R2020a.
Он напоминает блок C Caller с точки зрения простоты использования, но позволяет создавать код-обертку C вокруг импортируемых функций (таким образом, данный блок напоминает S-Function Builder). Но, скорее всего, основной сценарий использования блока C Function заключается не в вызове существующих функций C, а в написании небольших фрагментов кода на языке C, если такой код необходим в приложении. Например, может потребоваться доступ к аппаратным регистрам или встроенным функциям компилятора.
Не забудем добавить exlib.so/exlib.lib в настройки «Simulation Target -> Libraries» и #include «exlib.h» в настройки «Simulation Target -> Header file» в настройках модели.
После этого в настройках блока C Function надо добавить символ для входных данных с типом данных single и указать код вывода, запуска и завершения:

open_system('simlib_test_cfunction'); if isunix     set_param('simlib_test_cfunction','SimUserLibraries','exlib.so'); end sim('simlib_test_cfunction'); % Observe the results: type('exlib.txt'); 0.000000 1.000000 2.000000 3.000000 4.000000 5.000000 6.000000 7.000000 8.000000 9.000000 10.000000

Вызов разделяемых библиотек сгенерированных с помощью Embedded Coder

Одним из сценариев использования Embedded Coder является автоматическая генерация C-кода из модели Simulink и упаковка этого кода в разделяемую библиотеку. В документации также есть пример, который показывает, как автоматически генерировать разделяемую библиотеку и вызывать ее из внешнего приложения, написанного на C.
Обратите внимание, что если требуется просто запуск алгоритма или части алгоритма в виде C кода в той же модели Simulink, то лучше применить S-функции Simulink Coder или симуляции в режиме «ПО-в-контуре» (Software-in-the-Loop). Однако, если сгенерированная с помощью Embedded Coder разделяемая библиотека, используется для полунатурного моделирования, то может потребоваться интегрировать эту же библиотеку в более крупную модель, используемую другими разработчиками и таким образом защитить свою интеллектуальную собственность.
Работа с разделяемой библиотекой, сгенерированная Embedded Coder, ничем не отличается от работы с разделяемой библиотекой, которая была использована в статье. Рассмотрим простую модель, которая имеет два входа и один выход:

open_system('simlib_test_ert'); snapnow; 

После сборки модели мы получим файл .dll (.so в Linux), файл .lib (библиотека импорта для .dll) и файл .exp (файл экспорта для связи с .dll).

if isunix     set_param('simlib_test_ert','CustomHeaderCode','#include <stddef.h>'); end rtwbuild('simlib_test_ert'); ### Starting build procedure for: simlib_test_ert ### Successful completion of build procedure for: simlib_test_ert

Сгенерированная разделяемая библиотека экспортирует следующие символы:

ex_init double ex_step(double, double) simlib_test_ert_terminate

По умолчанию входы и выходы экспортируются как глобальные символы, а функции инициализации, шага и завершения следуют соглашению об именах моделей. При необходимости можно настроить прототипы функций, имена символов и прочее (обсуждение данных настроек выходит за рамки данной статьи). В этой модели прототип функции шага модели задан как Out1 = ex_step (In1, In2).
Для вызова этих функций нужно применить один из перечисленных выше методов. Например, можно использовать MATLAB Function (для простоты вызовем только функцию шага):

Посмотреть код

function y = fcn(u1, u2) %#codegen  % Generate library path on the fly coder.extrinsic('RTW.getBuildDir','fullfile'); buildDir = coder.const(RTW.getBuildDir('simlib_test_ert')); libpath = coder.const(buildDir.CodeGenFolder); incpath = coder.const(fullfile(buildDir.BuildDirectory,'simlib_test_ert.h')); % Shared library to link with if isunix     ext = '.so';     libname = ['simlib_test_ert',ext]; else     ext = '.lib';     libname = ['simlib_test_ert_',computer('arch'),ext]; end % Add the external library. Mark it as precompiled, so it won't appear as % makefile target during code generation. coder.updateBuildInfo('addLinkObjects',libname,libpath,1000,true,true); coder.updateBuildInfo('addIncludePaths',libpath); coder.cinclude(incpath);  % Initialize output y = 0; % Step y = coder.ceval('ex_step',u1,u2);

Запустим симуляцию и посмотрим на ее результаты:

open_system('simlib_test_callert'); sim('simlib_test_callert'); snapnow; 

Выводы

В этой статье описаны различные подходы к вызову разделяемых библиотек из моделей Simulink. Были рассмотрены как неявная, так и явная линковки. Все методы имеют свои плюсы и минусы, и их пригодность зависит от конкретного рабочего процесса, требований и целей.
Подход Legacy Code Tool лучше всего подходит при реализации неявной линковки с разделяемой библиотекой, поскольку в этом случае мы можем просто вызывать функции непосредственно из библиотеки, а компоновщик позаботится обо всем остальном.
Блок MATLAB System — это еще один подход, который обеспечивает следующие преимущества:

  • Отличное соответствие парадигме функций инициализации, шага и завершения, позволяя поддерживать все эти функции внутри самого блока, а не в масштабе модели
  • Позволяет добавлять пользовательский код к вызываемым внешним функцям
  • Позволяет вам оставаться в MATLAB и писать только код MATLAB
  • Блок MATLAB System является автономным и не требует дополнительных шагов для совместного использования (например, нет необходимости компилировать S-функцию)

Недостатком использования блока MATLAB Function является то, что он его применение может повлечь дополнительный оверхэд во время генерации кода. Таким образом, Legacy Code Tool и S-функции по-прежнему являются предпочтительными для генерации производственного кода.
Написанная вручную S-функция лучше всего подходит для реализации явной линковки с разделяемой библиотекой. В этом случае требуется использовать такие функции, как LoadLibrary/dlopen, GetProcAddress/dlsym, FreeLibrary/dlclose, чтобы иметь возможность вызывать функции.
Блок C Caller, появившийся в R2018b, безусловно, самый простой способ вызова функций C, но он имеет свои ограничения. То же самое можно сказать и о блоке C Function, который является новейшим дополнением в R2020a.

ссылка на оригинал статьи https://habr.com/ru/post/503908/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *