# 外部函数中记录的使用
介绍外部函数如何使用记录,以及使用过程中的常见问题和解决方式。
# 概述
外部函数用于集成其它语言开发的函数,Modelica规范对外部函数中能使用的参数有明确的规定,可以是内置类型及其数组、标量形式的记录。
MWORKS.Sysplorer 2023a及其以前的版本不支持外部函数中使用记录,2023b和其后的版本才增加了对记录的支持。
本文首先介绍外部函数如何使用记录,然后对可能遇到的问题及解决方案进行说明。
# 规范说明
Modelica规范对外部函数使用记录、记录与C语言映射有明确的规定,具体内容如下。
从规范中的说明可知,记录本身是数组、记录中有成员是数组是不支持的。记录作为参数时C语言中是指向记录对应结构体的指针。根据规范 12.9.2 的说明,返回记录时是传值的。
从规范的说明和示例来看,记录映射为同名的C语言结构体,记录的成员映射为结构体的同名成员。
# 使用说明
外部函数中使用记录最重要的一点是要将记录对应的C语言结构体定义包含进来,即外部函数必须有Include注解,在其中包含C语言源文件或头文件。除此之外,使用方法与其它类型参数的外部函数无异。
需要注意的是,C语言结构体的名字与记录的名字相同,结构体中成员的名字与记录中对应的组件名字相同。
# 记录以参数传递
记录以参数传递给C函数时,C语言函数对应的参数是指向相应结构体的指针。
示例,Modelica模型:
model Case13_1
record Rec
Real x;
Real y;
end Rec;
record R
Rec rec1;
Rec rec2;
Integer n;
end R;
function fcn
input R r;
output Real x;
external "C" x = FuncDemo(r)annotation (
Include = "#include \"Case13_1.c\""); // Include注解
end fcn;
R r(n = 2, rec1(x = time, y = sin(time)), rec2(x = 3.0, y = 5.4));
Real x = fcn(r);
end Case13_1;
Include 注解中包含了 C 语言源文件 Case13_1.c,其内容如下:
struct Rec
{
double x;
double y;
};
struct R
{
struct Rec rec1;
struct Rec rec2;
int n;
};
double FuncDemo(const struct R* r)
{
return r->n * r->rec1.x * r->rec1.y + r->rec2.x + r->rec2.y;
}
C 语言函数 FuncDemo 的参数 r 与 Modelica 外部函数 fcn 的输入参数 r 对应,是指向结构体的指针。
# 返回记录
当C以返回值的方式输出时,记录对应的结构体以值传递的方式返回。【MWORKS.Sysplorer 2024a之前的版本包括2023b不支持返回记录】
示例,Modelica模型:
model Case13_2
record R
Real x;
Integer m;
end R;
function func
input Real x;
output R r;
external "C" r = ReturnRecFunc(x)annotation (
Include = "#include \"Case13_2.h\"", // Include注解
Library="Case13_2");
end func;
R r = func(time);
end Case13_2;
Include 注解中包含了 C 语言头文件 Case13_2.h,其内容如下:
#ifndef CASE13_2_H
#define CASE13_2_H
struct R
{
double x;
int m;
}
struct R ReturnRecFunc(double x);
#endif /* CASE13_2_H */
源文件 Case13_2.c 内容如下,需编译为静态库 Case13_2.lib(visual c++) 或 libCase13_2.a(gcc):
#include "Case13_2.h"
struct R ReturnRecFunc(double x)
{
struct R r;
r.m=(int)x;
r.x=x-r.m;
return r;
}
C 语言函数 ReturnRecFunc 的返回值与 Modelica 外部函数 func 的输出参数 r 对应。
# 常见问题
# 如何解决记录重名?
Modelica中因为有包(packege)的机制,不同的库中可以有相同名字的记录类型,例如“P.Q.R”、“M.N.R”。一个Modelica模型中可以同时使用不同库中的同名记录。
package P
package Q
record R
Real x;
end R;
end Q;
end P;
package M
package N
record R
Real w;
end R;
end N;
end M;
model Case13_3
P.Q.R pqr(x=time);
M.N.R mnr(w=sin(time));
end Case13_3;
但是,这两个记录在同一个模型中的外部函数中使用就有问题,对应的结构体名字都是“R”,但是,结构体中的内容不同,这在C语言中是错误的。
/* P.Q.R */
struct R
{
double x;
}
/* M.N.R */
struct R
{
double w;
}
这种情况,遵循C语言中避免名字冲突的常用解决方法——在类型名之前加上适当的前缀。例如,同样要表示Real类型,MWORKS.Sysplorer的求解器中的类型命名为“MoReal”,而FMU规范中命名为“fmi2Real”,分别加了前缀“Mo”、“fmi2”。
按此思路,Modelica模型中记录类型修改为:
package P
package Q
record pqR
Real x;
end pqR;
end Q;
end P;
package M
package N
record mnR
Real w;
end mnR;
end N;
end M;
C语言结构体修改为:
/* P.Q.R */
struct pqR
{
double x;
}
/* M.N.R */
struct mnR
{
double w;
}
通常,前缀采用与商业实体或项目相关的前缀,或者两者的组合,以最大程度地减少命名冲突。
# MWORKS.Sysplorer 2023b 之前的版本如何在外部函数中使用记录?
外部函数记录参数2023b才开始支持,返回记录2024a才会支持,之前的版本不支持。
在MWORKS.Sysplorer 2023b之前,外部函数使用记录参数并没有报错,而且生成了对外部函数调用的语句,并且C编译器也没有报错,这本身是一个缺陷,但可以利用这个缺陷,强行在外部函数中使用记录,这种使用方式是不安全的,某些情况下出现问题难以定位。因为MWORKS.Sysplorer 2023b之前的版本对外部函数参数没有按规范生成结构体名和结构体成员,也就是说,MWORKS.Sysplorer生成的结构体与C语言中的结构体不相同。
如下的Modelica模型:
model Case13_4
record R
Integer n;
Real x;
end R;
function func
input R r;
output Real x;
external "C" x = ReturnRealFunc(r)annotation (
Include = "#include \"Case13_4.c\"");
end func;
R r(n=5,x=10.0);
Real x = func(r);
end Case13_4;
其中,Case13_4.c的内容如下:
struct R
{
int n;
double x;
};
double ReturnRealFunc(const struct R* r)
{
return r->x;
}
MWORKS.Sysplorer对记录R生成的结构体是这样的:
struct rec_Case13u_4a_R
{
MoInteger n_0_0;
MoReal x_0_0;
};
显然,与C语言中的外部函数中的结构体是不一样的。因为结构体的布局是一样的,一般情况下是可以得到正确结果。
现在将Case13_4.c的内容修改为:
struct R
{
int n;
double w;
double x;
};
double ReturnRealFunc(const struct R* r)
{
return r->x;
}
现在翻译没有任何报错,但结果不符合预期了。在多人维护模型库和C代码时,这种错误是有可能发生的,所以说是不安全的。MWORKS.Sysplorer 2023b不存在这个问题。
# 外部函数中使用了记录并且在 MWORKS.Sysplorer 2023b 之前的版本中能正常仿真的模型如何移植?
模型中记录、外部函数以及C语言结构体符合“使用说明”中的规定的情况下,模型可以直接在MWORKS.Sysplorer 2023b中仿真。如果不能仿真,一般情况下是没有包含C语言结构体定义所在的文件,在外部函数中增加Include注解即可。前述示例有Include注解的使用,在此不再赘述。
# 小结
MWORKS.Sysplorer 2023b支持外部函数使用记录与C语言函数传递数据,按规范的要求使用即可。之前的版本使用记录参数虽然能得到正确结果,但存在维护困难和潜在的难以发现的不安全因素,所以不建议使用。