目录
1.Modelica 基本类型与 C 语言的映射
2. 前缀与类型组合后的行为
3. 数组与 Record 的外部函数传参
3.1 数组传参
3.2 Record 传参
情况 1:Record 只含简单类型成员 → 可整体传
情况 2:Record 含数组成员 → 不能整体传(硬限制)
3.Include 注解:何时需要
4. output String 的内存开辟
4.1 输入 String:const char *
4.2 输出 String:const char ** + ModelicaAllocateString
4.3 完整工作流程
5. annotation
6. 自定义路径:modelica:// URI
1.Modelica 基本类型与 C 语言的映射
Modelica 外部函数调用 C 时,简单类型的默认映射如下:
| Modelica 类型 | C 输入类型 | C 输出类型(参数传递) |
|---|---|---|
Real | double | double * |
Integer | int | int * |
Boolean | int | int * |
String | const char * | const char **(或通过专用分配函数) |
Enumeration | int(从 1 开始) | int * |
| 景 | C 类型 | 内存管理责任方 | 关键操作 |
|---|---|---|---|
| String 作为输入 | const char * | Modelica 环境 | C 函数只读使用,无需分配/释放内存。 |
| String 作为输出 | const char ** | C 函数分配,Modelica 环境释放 | C 函数必须调用ModelicaAllocateString来为输出字符串分配内存。 |
2. 前缀与类型组合后的行为
- input:C 侧按值接收(double、int、const char *)
- output:C 侧接收指向存储位置的指针(double *、int *),C 函数向该地址写入结果
- protected 变量:在外部函数调用中与 output 同样传递(即传指针)
- constant / parameter:在外部函数上下文中,常量通常按 input 方式传入;但更本质的区别在于,parameter 和 constant 在模型方程计数中不计入“未知量”,而 output 类型的变量则参与变量计数
| 层面 | 决定什么 | parameter/constant的行为 | output的行为 |
|---|---|---|---|
| 类型 | 数据在 C 中的表示(double/int/ ...) | double | double |
| 前缀(函数调用侧) | 值传递还是地址传递 | 值传递(同 input) | 地址传递(指针) |
| 前缀(方程计数侧) | 是否计入“未知量” | 不计入(视为已知) | 计入(视为未知) |
3. 数组与 Record 的外部函数传参
3.1 数组传参
| 要点 | 规则 |
|---|---|
| 参数展开 | 一个数组实参 → C 端展开为1 个指针 + 每维 1 个size_t |
| 示例 | input Real a[3,4]→void foo(double *a, size_t dim1, size_t dim2) |
| 尺寸自动追加 | Modelica 调用里只写foo(a),尺寸由编译器自动补,不需要手写size(a,1) |
| 内存布局 | 默认row-major(行优先);调 Fortran 77 默认 column-major |
| 布局覆盖 | 需要列优先时加annotation(arrayLayout = "columnMajor") |
| 何时需要改布局 | C 代码按列优先索引(如从 Fortran 移植)时,否则读错元素 |
row-major vs column-major
二维数组在内存中是一维连续存储的。同一块内存,两种解读方式会导致完全不同的结果
Row-major(行优先)—— C 默认
内存顺序: [0][0], [0][1], [0][2], [0][3], [1][0], [1][1], ... 索引公式: addr(i,j) = a + (i * dim2 + j)Column-major(列优先)—— Fortran 默认
内存顺序: [0][0], [1][0], [2][0], [0][1], [1][1], ... 索引公式: addr(i,j) = a + (j * dim1 + i)arrayLayout 注解覆盖默认布局
external "C" foo(a) annotation(arrayLayout = "columnMajor");| 场景 | 是否需要arrayLayout |
|---|---|
C 代码用a[i*dim2+j](标准 C 风格) | 不需要,默认rowMajor |
C 代码用a[j*dim1+i](Fortran 风格移植) | 需要设columnMajor |
| 调用 Fortran 77 | 不需要,默认columnMajor |
3.2 Record 传参
情况 1:Record 只含简单类型成员 → 可整体传
record Point Real x; Real y; Integer tag; end Point; external "C" r = usePoint_C(p);C 端映射为同名 struct,成员按声明顺序排列
必须用 Include 提供 struct 定义,否则 C 编译器不认识 Point
头文件放在 Resources/Include/ 下(默认搜索路径),或通过 IncludeDirectory 指定
情况 2:Record 含数组成员 →不能整体传(硬限制)
原因:C struct 大小必须编译期固定,无法容纳动态维度 + size_t 尺寸信息
external "C" foo(traj.t0, traj.x, traj.y);- C 端按成员各自类型接收:标量用值,数组用“指针 + size_t”
- 不需要 struct Trajectory 定义,不需要 Include
3.Include 注解:何时需要
判断标准:生成的 C 代码里有没有 C 编译器不认识的标识符?
需要Include | 不需要Include |
|---|---|
参数含自定义struct(简单 record 整体传) | 参数全是基本类型(double/int/char*) |
C 函数用了<math.h>等标准库但生成代码未包含 | Record 含数组但拆开传(C 端无 struct 类型名) |
| C 函数依赖第三方库头文件 | C 实现自包含、无外部依赖 |
需要宏定义 /typedef/ 辅助函数原型 |
- Include:插入代码内容(#include、#define 等)
- IncludeDirectory:指定头文件搜索路径(modelica:// 格式)
4. output String 的内存开辟
String 的映射是整个类型体系中最特殊的一个,根源在于内存管理权责
| 场景 | C 类型 | 内存管理 | 关键操作 |
|---|---|---|---|
| String 输入 | const char * | Modelica 环境 | 只读,无需分配 |
| String 输出 | const char ** | C 分配,Modelica 释放 | 必须用ModelicaAllocateString |
4.1 输入 String:const char *
void foo(const char *s); // 只读,内存由 Modelica 环境管理C 函数只读使用,无需操心内存分配或释放
const char *s; // s 可改,*s 不能通过 s 改 const char **s; // s 可改,*s 可改,**s 不能通过 s 改 const char * const *s; // s 可改,*s 不能改,**s 不能通过 s 改4.2 输出 String:const char ** + ModelicaAllocateString
当 String 作为输出时,C 函数需要把一个新字符串的地址写回 Modelica,因此使用 const char **(指向字符指针的指针)
- 为什么不能简单 malloc
malloc 分配的内存所有权仍留在外部 C 代码,Modelica 仿真环境既不知道也不负责用 free 释放它,因此会造成内存泄漏;同时这块内存游离于仿真环境私有的栈/垃圾回收等内存管理机制之外,环境无法追踪、回收或安全清理,尤其在 DLL 卸载等场景下还可能因运行时/GC 销毁而变成无效指针导致崩溃,所以规范强制要求:返回非字面量字符串时,必须使用专用分配函数:由环境在合适时机安全释放
const char* ModelicaAllocateString(size_t len); // 分配 len 字节4.3 完整工作流程
// C 函数内部 void getMessage(const char **out_str) { const char *msg = "Hello from C"; size_t len = strlen(msg); // 步骤 1:必须用 ModelicaAllocateString 分配内存 char *buf = (char*)ModelicaAllocateString(len); // 步骤 2:拷贝内容 strcpy(buf, msg); // 步骤 3:把地址写回输出参数 *out_str = buf; }- C 函数调用 ModelicaAllocateString 申请内存
- 写入字符串内容
- 通过 const char ** 把地址写回 Modelica
- 外部函数返回后,Modelica 环境接管内存的释放
5. annotation
注解(annotation)是 Modelica 中用于存储模型附加元信息的机制。它不参与方程求解,但会指导工具链完成图形渲染、文档生成、外部资源定位、仿真设置、代码生成等任务
注解的语法与类修改(class modification)相同,使用圆括号(...),可以出现在类、组件、方程、函数等多个位置
class MyModel Real x; equation der(x) = -x; annotation ( Documentation( info = "这是一个一阶指数衰减模型", revisions = "v1.0 - 初始版本" ), Icon( coordinateSystem(extent = {{-100, -100}, {100, 100}}), graphics = { Rectangle(extent = {{-80, -50}, {80, 50}}, borderColor = {30, 91, 255}, fillColor = {219, 234, 254}) } ), uses(Modelica(version = "3.2.3")) ); end MyModel;一个类可以拥有多个注解,注解之间用逗号分隔。注解的值可以是字符串、数字、布尔值、数组、记录构造,甚至嵌套的注解调用
常见注解类型一览
| 注解 | 位置 | 说明 |
|---|---|---|
Documentation | 类 / 组件 | info描述、revisions版本历史、figures仿真图 |
Icon | 类 | 模型图标(面板内显示的图形) |
Diagram | 类 | 模型框图(内部组件连接图) |
Connector | connector | 连接器图标和图形设置 |
SmoothOrder | function | 函数的可微分阶数 |
derivative | function | 指定导函数名称 |
Inverse | function | 指定逆函数 |
Include | function / class | 外部函数声明所需的头文件内容注入 |
IncludeDirectory | function / class | 头文件搜索路径 |
Library | function / class | 链接库名称 |
LibraryDirectory | function / class | 编译好的库文件搜索路径 |
arrayLayout | function / class | 数组内存布局(rowMajor/columnMajor) |
uses(Modelica(...)) | package / class | 声明所使用的标准库版本 |
version/date/lastModified | class | 版本号与时间戳 |
Dialog | parameter / component | 参数对话框的group、tab分组 |
defaultComponentName | class | 图形化拖放时默认组件名前缀 |
PreferredView | class | 首选视图(icon/diagram/text) |
如果不写 IncludeDirectory、
LibraryDirectory,工具会在默认位置查找 Include 中引用的头文件:包含该外部函数的包(package.mo)所在目录下的 Resources/Include 子文件夹
6. 自定义路径:modelica:// URI
modelica:// 是一个逻辑 URI,它的含义是:“相对于某个 Modelica 库的根目录”。当工具(如 OpenModelica、Dymola)解析这个 URI 时,会自动将其转换为当前机器上的绝对文件路径
示例:假设你的库在包(MyPackage,包名)的 CustomLibs 文件夹下
annotation( Library = "MyAddDll", // 库名,不带前缀和后缀 LibraryDirectory = "modelica://MyPackage/CustomLibs" );