Llamar a bibliotecas compartidas desde Similink

Hola Habr!
Le presento la traducción de un artículo de mi colega Mikhail sobre métodos para invocar bibliotecas compartidas en Simulink. ¿Por qué fue creado en absoluto? El hecho es que muchas compañías ya tienen muchos modelos heredados que nos gustaría reutilizar y a menudo nos hacen preguntas: “¿Cómo puedo integrar el legado en Simulink? ¿Y si mi legado tiene la forma de una DLL? Por lo tanto, el artículo original fue escrito.
Debajo del gato, se discuten varias formas de llamar a las bibliotecas compartidas en Simulink.

Todos los cambios en el código se hicieron con el permiso de Mikhail y se hicieron para no sobrecargar el artículo.


Este artículo muestra cómo usar funciones de bibliotecas compartidas en modelos Simulink.
Digamos que necesitamos incrustar la biblioteca exlib en Simulink. Su código está contenido en las fuentes de exlib.c y exlib.h . Esta es una biblioteca muy simple que contiene tres funciones:
void exlib_init (void) : abre un archivo para escribir.
void exlib_print (datos flotantes) : escribe datos en un archivo.
void exlib_term (void) : cierra el archivo.

Asamblea de biblioteca


Primero, compila la biblioteca.
Este paso no es necesario si la biblioteca está precompilada y se entrega como un archivo binario. En este caso, puede ir inmediatamente al paso 3, pero recuerde que para trabajar con la biblioteca necesita obtener el archivo .dll (o .so para Linux) y el archivo de encabezado correspondiente (.h) del proveedor de la biblioteca. Para Windows, también necesitará un archivo .lib, que no es una biblioteca estática, sino la llamada biblioteca de importación. Esta biblioteca de importación es necesaria para la vinculación implícita.
Primero, utilizaremos el comando MATLAB mex, que llama al compilador de host activo actual para compilar la biblioteca compartida (exlib.dll en Windows y exlib.so en Linux). Es importante asegurarse de que el comando se haya ejecutado primero.
mex -setup


para seleccionar un compilador compatible .
Ahora use mex para compilar la biblioteca:

Código para construir una biblioteca
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.



Comprobando la biblioteca compilada


Después de la compilación, debe asegurarse de que la biblioteca resultante se pueda cargar y usar en MATLAB:

Código para revisar la biblioteca
% 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


Ahora que nos hemos asegurado de que todo funcione, ¡comencemos con Simulink!

Invocar una biblioteca compartida utilizando funciones S


Las funciones S le permiten usar el código C en Simulink. Este enfoque permite al usuario desarrollar cualquier código C necesario para cargar la biblioteca y llamar a sus funciones. Luego, este código se compila en un archivo binario reconocido por Simulink y vinculado a una biblioteca compartida. Este binario se llama función S. Simulink tiene un bloque para llamar a esta función S. Existen varios enfoques para crear funciones S en Simulink.

Usar la herramienta de código heredado


El primer enfoque es usar Legacy Code Tool , una herramienta que lo ayuda a crear automáticamente funciones S de acuerdo con las especificaciones creadas con MATLAB. En este ejemplo, la función S está vinculada a la biblioteca de importación (en Windows necesitamos el archivo * .lib correspondiente) y se llama a las funciones de la biblioteca. Este enfoque se llama vinculación implícita.
Primero, debe inicializar la estructura de la herramienta de código heredado
Código para inicializar la estructura de la herramienta de código heredado
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';



Cree una función S, compílela y enlácela:
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


Finalmente, cree el bloque Simulink:
legacy_code('slblock_generate',specs);


Ejecute la simulación:
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


Escribir funciones S manualmente


El segundo enfoque es escribir sus propias funciones S para cargar una biblioteca compartida y llamar a funciones desde esta biblioteca. Este enfoque utilizará enlaces explícitos, también llamados enlaces de tiempo de ejecución. La solución más fácil es adaptar la función S que se generó automáticamente anteriormente con la herramienta Legacy Code Tool. En este ejemplo, se modifican las funciones mdlStart , mdlOutputs y mdlTerminate .
Así es como se ven estas funciones después de la modificación:
Ver código
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
}



Compile la función S resultante:
if isunix
    mex('sfun_exlib_dyn.c','-ldl');
else
    mex('sfun_exlib_dyn.c');
end
Building with 'gcc'.
MEX completed successfully.


Y ejecuta la simulación:
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


Usando S-Function Builder


Otro enfoque es usar el bloque S-Function Builder . Esta es una unidad especial que puede verse como un cruce entre la herramienta de código heredado y las funciones S escritas a mano en términos de complejidad. S-Function Builder proporciona una interfaz gráfica que describe las características de su función S, y la función S se crea automáticamente.
Existen algunas limitaciones de rendimiento y problemas con el uso de S-Function Builder. Actualmente, esto se considera un enfoque obsoleto para crear funciones S. Se recomienda utilizar el bloque de función C, que apareció en el lanzamiento de R2020a y se analiza más adelante.

Invocar una biblioteca compartida utilizando la función MATLAB


El bloque de funciones MATLAB le permite usar el lenguaje MATLAB para describir un algoritmo personalizado en Simulink.
Para llamar a la biblioteca compartida desde la función MATLAB, use la función coder.ceval , que solo se puede usar en el cuerpo de la función MATLAB (y no en MATLAB). Coder.ceval no requiere MATLAB Coder para funcionar.
Código para el bloque de funciones MATLAB que llama a la biblioteca compartida:
Ver código
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



Este enfoque tiene un inconveniente: la falta de una buena forma de llamar a la función de finalización al final de la simulación (en comparación con el bloque del sistema MATLAB).
Puede definir la función de finalización para el modelo estableciendo exlib_term (); en la configuración del modelo en la categoría Objetivo de simulación -> Código personalizado -> Terminar función .

NOTA . Si el parámetro "Importar código personalizado" está configurado en la configuración del modelo, debe especificar todas las dependencias de código en el panel "Simulación de destino" (en lugar de usar coder.cinclude y coder.updateBuildInfo ). Si este parámetro no está configurado, puede combinar la configuración de Simulation Target, coder.cinclude y coder.updateBuildInfo.


Otra forma es mantener dependencias usandocoder.cinclude y coder.updateBuildInfo , y llame a exlib_term () por convención, como se demostró en el ejemplo anterior.
Ejecute la simulación:
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


Invocar una biblioteca compartida desde Stateflow


Si desea utilizar las funciones de la biblioteca compartida en los diagramas de Stateflow , estas funciones deben llamarse directamente desde Stateflow. La documentación de Stateflow tiene ejemplos que muestran cómo hacer esto.
Llamar a una función externa en Stateflow es muy simple: debe especificar el nombre de la función en el diagrama de Stateflow:


además, debe configurar los parámetros del modelo para que Stateflow sepa dónde buscar estas funciones externas. En la configuración de Simulation Target -> Código personalizado -> Bibliotecas, debe ingresar exlib.lib (o exlib.so en Linux) . En Simulation Target -> Custom Code -> Header File, debe ingresar #include "exlib.h" . También es importante no olvidar especificar la función de finalización. AObjetivo de simulación -> Código personalizado -> La función Terminar debe especificar exlib_term (); .
Ejecute la simulación:
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


Tenga en cuenta que la información contenida en esta sección se aplica a los diagramas de Stateflow que utilizan el lenguaje de acción C . Si el diagrama de Stateflow usa el lenguaje de acción MATLAB , entonces se requiere coder.ceval , como es el caso con la función MATLAB.

Invocar una biblioteca compartida utilizando el bloque del sistema MATLAB


El bloque del sistema MATLAB le permite usar objetos del sistema en Simulink. Se puede encontrar más información sobre este bloque en la documentación.
El soporte para objetos del sistema apareció en Simulink en el lanzamiento de R2013b. Muchas personas usan objetos del sistema porque facilitan la definición de las funciones de inicialización, simulación y finalización. Además, los objetos del sistema pueden usar el código MATLAB auxiliar para estas funciones, por ejemplo, para los datos de procesamiento previo y posterior de una función de paso, todo sin escribir el código C. Así
es como se ve el objeto del sistema utilizado en el bloque del sistema MATLAB:
Código de objeto del sistema
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



Ejecute la simulación:
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


Llamar a bibliotecas compartidas usando el bloque de llamadas C


El bloque C Caller le permite llamar a funciones C directamente desde Simulink. Se puede encontrar más información sobre este bloque en la documentación.
Este es un enfoque relativamente nuevo que apareció por primera vez en MATLAB R2018b. Su objetivo principal es hacer que las llamadas a funciones y las bibliotecas C en Simulink sean extremadamente simples. Pero tiene limitaciones, sobre las que puede leer en la documentación de este bloque.
Después de que exlib.so/exlib.lib se haya agregado a Simulation Target -> Libraries y #include "exlib.h" en Simulation Target -> Header en la configuración del modelo, simplemente haga clic en el botón "Actualizar código personalizado" en el bloque C Caller, para ver todas las funciones contenidas en la biblioteca.
Después de seleccionar la función exlib_print , el cuadro de diálogo de especificación de puerto se completa automáticamente:

Y nuevamente, debe agregar las llamadas a la función exlib_init y exlib_term al Objetivo de simulación. También puede agregar un par de otros bloques de C Caller para llamar directamente a las funciones de inicialización y finalización. Estos bloques C Caller deben colocarse en los subsistemas Initialize Function y Terminate Function. También puede considerar el siguiente ejemplo de Stateflow: Programar subsistemas para ejecutar en momentos específicos
Ejecute la simulación:
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


Invocación de bibliotecas compartidas utilizando el bloque de función C


La última adición a la integración de código externo en Simulink es el C Función bloque . Él apareció en el R2020a.
Se asemeja al bloque C Caller en términos de facilidad de uso, pero le permite crear un contenedor C alrededor de las funciones importadas (por lo tanto, este bloque se asemeja a S-Function Builder). Pero, lo más probable, el escenario principal para usar el bloque de función C no es llamar a funciones C existentes, sino escribir pequeños fragmentos de código C, si dicho código es necesario en la aplicación. Por ejemplo, es posible que necesite acceso a los registros de hardware o las funciones integradas del compilador.
No olvide agregar exlib.so/exlib.lib a la configuración "Simulación de destino -> Bibliotecas" y #include "exlib.h"en la configuración "Objetivo de simulación -> Archivo de encabezado" en la configuración del modelo.
Después de eso, en la configuración del bloque de función C, debe agregar un carácter para los datos de entrada con el tipo de datos único y especificar el código de salida, inicio y finalización:


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


Invocación de bibliotecas compartidas generadas con el codificador incorporado


Uno de los escenarios para usar Embedded Coder es la generación automática de código C a partir del modelo Simulink y el empaquetado de este código en una biblioteca compartida. La documentación también tiene un ejemplo que muestra cómo generar automáticamente una biblioteca compartida y llamarla desde una aplicación externa escrita en C.
Tenga en cuenta que si solo necesita ejecutar el algoritmo o parte del algoritmo como código C en el mismo modelo Simulink, entonces es mejor usar Funciones S de Simulink Coder o simulación de software en el bucle. Sin embargo, si la biblioteca compartida generada por Embedded Coder se usa para el modelado seminatural, puede ser necesario integrar la misma biblioteca en el modelo más grande utilizado por otros desarrolladores y así proteger su propiedad intelectual.
Trabajar con una biblioteca compartida generada por Embedded Coder no es diferente de trabajar con una biblioteca compartida, que se utilizó en el artículo. Considere un modelo simple que tiene dos entradas y una salida:
open_system('simlib_test_ert');
snapnow;



Después de ensamblar el modelo, obtenemos un archivo .dll (.so en Linux), un archivo .lib (una biblioteca de importación para .dll) y un archivo .exp (un archivo de exportación para comunicarse con .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


La biblioteca compartida generada exporta los siguientes caracteres:
ex_init
double ex_step(double, double)
simlib_test_ert_terminate


De manera predeterminada, las entradas y salidas se exportan como caracteres globales, y las funciones de inicialización, paso y finalización siguen la convención de nomenclatura del modelo. Si es necesario, puede configurar prototipos de funciones, nombres de símbolos y más (una discusión sobre estas configuraciones está más allá del alcance de este artículo). En este modelo, el prototipo de la función de paso del modelo se define como Out1 = ex_step (In1, In2) .
Para llamar a estas funciones, debe usar uno de los métodos enumerados anteriormente. Por ejemplo, puede usar la función MATLAB (por simplicidad solo llamamos a la función de paso):
Ver código
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);



Ejecute la simulación y observe sus resultados:
open_system('simlib_test_callert');
sim('simlib_test_callert');
snapnow;



recomendaciones


Este artículo describe varios enfoques para invocar bibliotecas compartidas de modelos Simulink. Se consideraron enlaces implícitos y explícitos. Todos los métodos tienen sus pros y sus contras, y su idoneidad depende del flujo de trabajo específico, los requisitos y los objetivos.
El enfoque de Legacy Code Tool funciona mejor cuando se implementa un enlace implícito con una biblioteca compartida, porque en este caso solo podemos llamar funciones directamente desde la biblioteca, y el enlazador se encargará del resto.
El bloque del sistema MATLAB es otro enfoque que proporciona los siguientes beneficios:
  • Excelente cumplimiento del paradigma de las funciones de inicialización, paso y finalización, lo que le permite mantener todas estas funciones dentro del bloque en sí, y no en la escala del modelo.

  • MATLAB MATLAB
  • MATLAB System (, S-)

La desventaja de usar el bloque de funciones MATLAB es que su uso puede generar una sobrecarga adicional durante la generación del código. Por lo tanto, las herramientas Legacy Code Tool y S todavía se prefieren para generar código de producción.
Una función S escrita a mano es la más adecuada para implementar un enlace explícito a una biblioteca compartida. En este caso, debe usar funciones como LoadLibrary / dlopen , GetProcAddress / dlsym , FreeLibrary / dlclose para poder llamar a funciones.
El bloque C Caller introducido en R2018b es, con mucho, la forma más fácil de llamar a funciones C, pero tiene sus limitaciones. Lo mismo puede decirse del bloque de función C.que es la última incorporación al R2020a.

Descargue fuentes y modelos aquí

All Articles