diff --git a/RationaleMCP/0031/ReadMe.md b/RationaleMCP/0031/ReadMe.md index 06f7df4ee..ca668d23a 100644 --- a/RationaleMCP/0031/ReadMe.md +++ b/RationaleMCP/0031/ReadMe.md @@ -81,6 +81,7 @@ These are subtopics that are considered necessary to resolve for a first version - [x] Remove byte order mark, as it is already deprecated in full Modelica. [PR with discussion](https://github.com/modelica/ModelicaSpecification/pull/3528) - [ ] Base Modelica package shall have no dependencies on other loaded classes. - [ ] Management of resources and Modelica URIs. +- [ ] External function interface and external objects. ### Base Modelica 0.1+…1.0 (future MCPs) In future minor versions of Base Modelica 1, we could improve the language by incorporating smaller improvements that were not considered necessary for version 1.0. diff --git a/RationaleMCP/0031/differences.md b/RationaleMCP/0031/differences.md index c81a5da47..267590a2b 100644 --- a/RationaleMCP/0031/differences.md +++ b/RationaleMCP/0031/differences.md @@ -249,7 +249,7 @@ This change was made to support the [changed definitions of _constant expression Base Modelica functions cannot have function default arguments. A tool producing Base Modelica from full Modelica can accommodate this by automatically generating a helper function for every present subset of arguments used in the model. -(The helpers can be omitted if the defaults are literal or otherwise independent of the other inputs, since those values can be added in each call. +(The helpers can be omitted if the defaults are literal or otherwise independent of the other inputs, since those values can be added in each call. No helper function needs to be created for argument combinations that aren't used in the model, which means that the potential combinatorial explosion of possible argument combinations is avoided.) For example, consider this full Modelica model: @@ -843,7 +843,7 @@ model A Real x(start = 1.0); Real y; Real z; - equation + equation y = 5 * x; z = 7 * x; x + y + z = sin(time + x + y + z); @@ -1304,6 +1304,45 @@ Accordingly, a function component declaration which is neither input nor output The new annotation `protected = true` provides a standardized way to indicate that a component declaration in Base Modelica comes from a protected section in the full Modelica model. See [`protected` annotation](annotations.md#protected). +## External Functions + +Base Modelica restricts the full Modelica external function interface ([12.9](https://specification.modelica.org/master/functions.html#external-function-interface)) in several ways. + +**Language.** +Only `"C"` is permitted as the language specification; `"FORTRAN 77"` is dropped. +See [grammar](grammar.md#B22-Class-definition) for the restricted `_language-specification_` rule. + +**Annotations.** +The `Include`, `IncludeDirectory`, and `SourceDirectory` annotations are not allowed in Base Modelica. +Only binary distribution is supported, so the permitted annotations on the `external` clause are `Library`, `LibraryDirectory`, and `License`. +Both `Library` and `LibraryDirectory` must always be given explicitly — no defaults are provided. +See [grammar](grammar.md#B22-Class-definition) for the `_external-annotation_` rule. + +**URI scheme.** +The `modelica://` URI scheme is replaced by `base-modelica:/`, which is always relative to the directory of the current `.bmo` file. +This removes any dependency on package path lookups. + +**Library layout.** +The platform (e.g. `linux64`, `win64`) must be given as a mandatory subdirectory of the `LibraryDirectory` path. +Placing libraries directly in `LibraryDirectory` is not allowed. +Using the model name as part of the path (e.g. `base-modelica:/Example/PackageA/Library`) is recommended to prevent naming conflicts when two packages provide a library with the same filename. + +**External objects.** +The `class` keyword is the only class prefix allowed Base Modelica to support external objects that extend `ExternalObject`. +The `constructor` and `destructor` functions follow the same rules as in full Modelica ([12.9.8](https://specification.modelica.org/master/functions.html#external-objects)); no further changes are needed. + +### Rationale + +The introduction of the new URI scheme `base-modelica:/` removes the ambiguity of having a class that extends `ExternalObject`. +With the need to explicitly state what the name of the library is and where the library is located, it is completely clear where to look for the external function during linkage. +No guesswork about `MODELICAPATH` lookups or conflicting library versions is needed. + +This simplifies the work a Base Modelica tool has to perform. +For example, a source code library needs to be compiled for the target architecture of the host system before a simulation executable can link to it. +Consequently, this removes the need to compile arbitrary C files during transformation from Base Modelica to a simulation executable. + +All compilation of source code has to be handled by the tool lowering Modelica to Base Modelica or the modeler. +The restricted set of Modelica annotations is the simplest way to build a simulation executable from Base Modelica. ## Clock partitions diff --git a/RationaleMCP/0031/examples/BaseModelica/Example.bmo b/RationaleMCP/0031/examples/BaseModelica/Example.bmo new file mode 100644 index 000000000..1d03b81ac --- /dev/null +++ b/RationaleMCP/0031/examples/BaseModelica/Example.bmo @@ -0,0 +1,36 @@ +//! base 0.1.0 +package 'Example' + function 'PackageA.ExternalFunc1' "Include header file for library implementation" + input Real 'x'; + output Real 'y'; + external "C" 'y' = ExternalFunc1_ext('x') annotation( + Library = "ExternalLib1", + LibraryDirectory = "base-modelica:/Example/PackageA/Library"); + end 'PackageA.ExternalFunc1'; + + function 'PackageA.ExternalFunc2' "Include header file for library implementation" + input Real 'x'; + output Real 'y'; + external "C" annotation( + Library = "ExternalLib2", + LibraryDirectory = "base-modelica:/Example/PackageA/Library"); + end 'PackageA.ExternalFunc2'; + + function 'PackageB.ExternalFunc1' "Source file was compiled into a library by lowering tool" + input Real 'x'; + output Real 'y'; + external "C" annotation( + Library = "ExternalLib1", + LibraryDirectory = "base-modelica:/Example/PackageB/Library"); + end 'PackageB.ExternalFunc1'; + + model 'Example' + Real 'x'(start = 1.0); + Real 'y'(start = 2.0); + Real 'z'(start = 3.0); + equation + der('x') = -'PackageA.ExternalFunc1'('x'); + der('y') = -'PackageA.ExternalFunc2'('y'); + der('z') = -'PackageB.ExternalFunc1'('z'); + end 'Example'; +end 'Example'; diff --git a/RationaleMCP/0031/examples/BaseModelica/Example/PackageA/Library/linux64/libExternalLib1.a b/RationaleMCP/0031/examples/BaseModelica/Example/PackageA/Library/linux64/libExternalLib1.a new file mode 100644 index 000000000..b493b20d0 Binary files /dev/null and b/RationaleMCP/0031/examples/BaseModelica/Example/PackageA/Library/linux64/libExternalLib1.a differ diff --git a/RationaleMCP/0031/examples/BaseModelica/Example/PackageA/Library/linux64/libExternalLib2.so b/RationaleMCP/0031/examples/BaseModelica/Example/PackageA/Library/linux64/libExternalLib2.so new file mode 100755 index 000000000..1b7fe13cf Binary files /dev/null and b/RationaleMCP/0031/examples/BaseModelica/Example/PackageA/Library/linux64/libExternalLib2.so differ diff --git a/RationaleMCP/0031/examples/BaseModelica/Example/PackageB/Library/linux64/libExternalLib1.a b/RationaleMCP/0031/examples/BaseModelica/Example/PackageB/Library/linux64/libExternalLib1.a new file mode 100644 index 000000000..e87b12d82 Binary files /dev/null and b/RationaleMCP/0031/examples/BaseModelica/Example/PackageB/Library/linux64/libExternalLib1.a differ diff --git a/RationaleMCP/0031/examples/BaseModelica/ExternalObjectExample.bmo b/RationaleMCP/0031/examples/BaseModelica/ExternalObjectExample.bmo new file mode 100644 index 000000000..cd57670ce --- /dev/null +++ b/RationaleMCP/0031/examples/BaseModelica/ExternalObjectExample.bmo @@ -0,0 +1,40 @@ +//! base 0.1.0 +package 'ExternalObjectExample' + function 'Multiplication.multiply' "Multiply x by the factor stored in the external object" + input 'Multiplication.Multiplier' 'm'; + input Real 'x'; + output Real 'y'; + external "C" 'y' = Multiplier_multiply('m', 'x') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end 'Multiplication.multiply'; + + class 'Multiplication.Multiplier' + extends ExternalObject; + + function constructor "Allocate and initialise the multiplier" + input Real 'factor'; + output 'Multiplication.Multiplier' 'obj'; + external "C" 'obj' = Multiplier_constructor('factor') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end constructor; + + function destructor "Free the multiplier" + input 'Multiplication.Multiplier' 'obj'; + external "C" Multiplier_destructor('obj') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end destructor; + + end 'Multiplication.Multiplier'; + + model 'ExternalObjectExample' "Multiply a decaying signal by a constant factor stored in an external object" + 'Multiplication.Multiplier' 'm' = 'Multiplication.Multiplier'(3.0); + Real 'x'(start = 1.0) "Decaying signal"; + Real 'y' "Scaled output: y = 3 * x"; + equation + der('x') = -'x'; + 'y' = 'Multiplication.multiply'('m', 'x'); + end 'ExternalObjectExample'; +end 'ExternalObjectExample'; diff --git a/RationaleMCP/0031/examples/BaseModelica/ExternalObjectExample/.gitignore b/RationaleMCP/0031/examples/BaseModelica/ExternalObjectExample/.gitignore new file mode 100644 index 000000000..4b8cb9bf2 --- /dev/null +++ b/RationaleMCP/0031/examples/BaseModelica/ExternalObjectExample/.gitignore @@ -0,0 +1,5 @@ +*.a +*.so +*.dylib +*.lib +*.dll diff --git a/RationaleMCP/0031/examples/BaseModelica/ExternalObjectExample_2.bmo b/RationaleMCP/0031/examples/BaseModelica/ExternalObjectExample_2.bmo new file mode 100644 index 000000000..b35d7854d --- /dev/null +++ b/RationaleMCP/0031/examples/BaseModelica/ExternalObjectExample_2.bmo @@ -0,0 +1,39 @@ +//! base 0.1.0 +package 'ExternalObjectExample' + function 'Multiplication.multiply' "Multiply x by the factor stored in the external object" + input 'Multiplication.Multiplier' 'm'; + input Real 'x'; + output Real 'y'; + external "C" 'y' = Multiplier_multiply('m', 'x') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end 'Multiplication.multiply'; + + function 'Multiplication.Multiplier.constructor' "Allocate and initialise the multiplier" + input Real 'factor'; + output 'Multiplication.Multiplier' 'obj'; + external "C" 'obj' = Multiplier_constructor('factor') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end 'Multiplication.Multiplier.constructor'; + + function 'Multiplication.Multiplier.destructor' "Free the multiplier" + input 'Multiplication.Multiplier' 'obj'; + external "C" Multiplier_destructor('obj') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end 'Multiplication.Multiplier.destructor'; + + model 'ExternalObjectExample' "Multiply a decaying signal by a constant factor stored in an external object" + 'Multiplication.Multiplier' 'm' = 'Multiplication.Multiplier.constructor'(3.0); + Real 'x'(start = 1.0) "Decaying signal"; + Real 'y' "Scaled output: y = 3 * x"; + equation + der('x') = -'x'; + 'y' = 'Multiplication.multiply'('m', 'x'); + + when terminate then // New construct + 'Multiplication.Multiplier.destructor'('m'); + end when; + end 'ExternalObjectExample'; +end 'ExternalObjectExample'; diff --git a/RationaleMCP/0031/examples/Modelica/Example.mo b/RationaleMCP/0031/examples/Modelica/Example.mo new file mode 100644 index 000000000..08fe16f08 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/Example.mo @@ -0,0 +1,9 @@ +model Example + Real x(start = 1.0); + Real y(start = 2.0); + Real z(start = 3.0); +equation + der(x) = -PackageA.ExternalFunc1(x); + der(y) = -PackageA.ExternalFunc2(y); + der(z) = -PackageB.ExternalFunc1(z); +end Example; diff --git a/RationaleMCP/0031/examples/Modelica/ExternalObjectExample.mo b/RationaleMCP/0031/examples/Modelica/ExternalObjectExample.mo new file mode 100644 index 000000000..78f23ab8f --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/ExternalObjectExample.mo @@ -0,0 +1,11 @@ +model ExternalObjectExample "Multiply a decaying signal by a constant factor stored in an external object" + // The external object is created once with factor = 3.0. + // It lives on the C heap for the duration of the simulation. + Multiplication.Multiplier m = Multiplication.Multiplier(factor = 3.0); + + Real x(start = 1.0) "Decaying signal"; + Real y "Scaled output: y = 3 * x"; +equation + der(x) = -x; + y = Multiplication.multiply(m, x); +end ExternalObjectExample; diff --git a/RationaleMCP/0031/examples/Modelica/Multiplication/.gitignore b/RationaleMCP/0031/examples/Modelica/Multiplication/.gitignore new file mode 100644 index 000000000..a196733d2 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/Multiplication/.gitignore @@ -0,0 +1,3 @@ +Resources/Library/linux64/ +Resources/Library/win64/ +Resources/Source/build-*/ diff --git a/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Include/Multiplier.h b/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Include/Multiplier.h new file mode 100644 index 000000000..8a6323fa3 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Include/Multiplier.h @@ -0,0 +1,9 @@ +/* Multiplier.h – opaque external object storing a multiplication factor */ +#ifndef MULTIPLIER_H +#define MULTIPLIER_H + +void *Multiplier_constructor(double factor); +void Multiplier_destructor(void *obj); +double Multiplier_multiply(void *obj, double x); + +#endif /* MULTIPLIER_H */ diff --git a/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/CMakeLists.txt b/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/CMakeLists.txt new file mode 100644 index 000000000..a180e7311 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.15) +project(Multiplication C) + +# Supported targets (set via toolchain file or detected automatically): +# linux64 – 64-bit Linux (native: -DCMAKE_TOOLCHAIN_FILE=toolchains/linux64.cmake) +# win64 – 64-bit Windows (cross: -DCMAKE_TOOLCHAIN_FILE=toolchains/win64.cmake) +# +# Usage: +# Native Linux 64-bit build: +# cmake -B build-linux64 -DCMAKE_TOOLCHAIN_FILE=toolchains/linux64.cmake +# cmake --build build-linux64 +# +# Cross-compile to win64 (requires mingw-w64): +# cmake -B build-win64 -DCMAKE_TOOLCHAIN_FILE=toolchains/win64.cmake +# cmake --build build-win64 + +if(NOT DEFINED MODELICA_PLATFORM) + if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(MODELICA_PLATFORM "win64") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(MODELICA_PLATFORM "linux64") + else() + set(MODELICA_PLATFORM "unknown") + endif() +endif() + +message(STATUS "Modelica platform tag: ${MODELICA_PLATFORM}") + +set(LIBRARY_OUTPUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../Library/${MODELICA_PLATFORM}") + +# ── Multiplication (static) ────────────────────────────────────────────────── + +add_library(Multiplication STATIC Multiplier.c) +target_include_directories(Multiplication PRIVATE ../Include) + +set_target_properties(Multiplication PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${LIBRARY_OUTPUT_DIR}" +) diff --git a/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/Multiplier.c b/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/Multiplier.c new file mode 100644 index 000000000..420ddfce8 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/Multiplier.c @@ -0,0 +1,22 @@ +/* Multiplier.c – external object that stores and applies a multiplication factor */ +#include +#include "Multiplier.h" + +typedef struct { + double factor; +} Multiplier; + +void *Multiplier_constructor(double factor) { + Multiplier *m = (Multiplier *)malloc(sizeof(Multiplier)); + m->factor = factor; + return (void *)m; +} + +void Multiplier_destructor(void *obj) { + free(obj); +} + +double Multiplier_multiply(void *obj, double x) { + Multiplier *m = (Multiplier *)obj; + return m->factor * x; +} diff --git a/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/toolchains/linux64.cmake b/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/toolchains/linux64.cmake new file mode 100644 index 000000000..e9a0e1f59 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/toolchains/linux64.cmake @@ -0,0 +1,6 @@ +# Toolchain: Linux 64-bit +# Native build — no extra packages required on a 64-bit Linux host. +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(MODELICA_PLATFORM "linux64" CACHE STRING "" FORCE) diff --git a/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/toolchains/win64.cmake b/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/toolchains/win64.cmake new file mode 100644 index 000000000..cb6ce4584 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/Multiplication/Resources/Source/toolchains/win64.cmake @@ -0,0 +1,14 @@ +# Toolchain: Windows 64-bit via MinGW-w64 +# Requires: mingw-w64 (e.g. apt install mingw-w64) +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) + +set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(MODELICA_PLATFORM "win64" CACHE STRING "" FORCE) diff --git a/RationaleMCP/0031/examples/Modelica/Multiplication/package.mo b/RationaleMCP/0031/examples/Modelica/Multiplication/package.mo new file mode 100644 index 000000000..a5a472882 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/Multiplication/package.mo @@ -0,0 +1,41 @@ +package Multiplication + + class Multiplier "External object storing a multiplication factor" + extends ExternalObject; + + function constructor "Allocate and initialise the multiplier" + input Real factor; + output Multiplier obj; + external "C" + obj = Multiplier_constructor(factor) + annotation( + Library = "Multiplication", + Include = "#include \"Multiplier.h\"", + LibraryDirectory = "modelica://Multiplication/Resources/Library"); + end constructor; + + function destructor "Free the multiplier" + input Multiplier obj; + external "C" + Multiplier_destructor(obj) + annotation( + Library = "Multiplication", + Include = "#include \"Multiplier.h\"", + LibraryDirectory = "modelica://Multiplication/Resources/Library"); + end destructor; + + end Multiplier; + + function multiply "Multiply x by the factor stored in the external object" + input Multiplier m; + input Real x; + output Real y; + external "C" + y = Multiplier_multiply(m, x) + annotation( + Library = "Multiplication", + Include = "#include \"Multiplier.h\"", + LibraryDirectory = "modelica://Multiplication/Resources/Library"); + end multiply; + +end Multiplication; diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Include/ExternalFunc1.h b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Include/ExternalFunc1.h new file mode 100644 index 000000000..5e222bb44 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Include/ExternalFunc1.h @@ -0,0 +1,7 @@ +// File ExternalFunc1.h +#ifndef EXTERNALFUNC1_H +#define EXTERNALFUNC1_H + +double ExternalFunc1_ext(double x); + +#endif diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Include/ExternalFunc2.h b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Include/ExternalFunc2.h new file mode 100644 index 000000000..38a58e7b4 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Include/ExternalFunc2.h @@ -0,0 +1,23 @@ +// File ExternalFunc2.h +#ifdef __cplusplus +extern "C" { +#endif +#ifdef _MSC_VER +#ifdef EXTERNAL_FUNCTION_EXPORT +# define EXTLIB2_EXPORT __declspec( dllexport ) +#else +# define EXTLIB2_EXPORT __declspec( dllimport ) +#endif +#elif __GNUC__ >= 4 + /* In gnuc, all symbols are by default exported. It is still often useful, + to not export all symbols but only the needed ones */ +# define EXTLIB2_EXPORT __attribute__ ((visibility("default"))) +#else +# define EXTLIB2_EXPORT +#endif + +EXTLIB2_EXPORT double ExternalFunc2(double x); + +#ifdef __cplusplus +} +#endif diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/linux64/libExternalLib1.a b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/linux64/libExternalLib1.a new file mode 100644 index 000000000..b493b20d0 Binary files /dev/null and b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/linux64/libExternalLib1.a differ diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/linux64/libExternalLib2.so b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/linux64/libExternalLib2.so new file mode 100755 index 000000000..1b7fe13cf Binary files /dev/null and b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/linux64/libExternalLib2.so differ diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/win64/libExternalLib1.a b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/win64/libExternalLib1.a new file mode 100644 index 000000000..919be90d2 Binary files /dev/null and b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/win64/libExternalLib1.a differ diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/win64/libExternalLib2.dll b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/win64/libExternalLib2.dll new file mode 100755 index 000000000..8f3a6af31 Binary files /dev/null and b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/win64/libExternalLib2.dll differ diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/win64/libExternalLib2.dll.a b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/win64/libExternalLib2.dll.a new file mode 100644 index 000000000..a9665abf4 Binary files /dev/null and b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Library/win64/libExternalLib2.dll.a differ diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/.gitignore b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/.gitignore new file mode 100644 index 000000000..e0cd5b7a8 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/.gitignore @@ -0,0 +1,2 @@ +# CMake build directories +build-*/ diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/CMakeLists.txt b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/CMakeLists.txt new file mode 100644 index 000000000..4df0fc4e6 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/CMakeLists.txt @@ -0,0 +1,63 @@ +cmake_minimum_required(VERSION 3.15) +project(PackageA C) + +# Determine platform tag and output directory +# Supported targets (set via toolchain file or detected automatically): +# linux64 – 64-bit Linux (native or cross: -DCMAKE_TOOLCHAIN_FILE=toolchains/linux64.cmake) +# win64 – 64-bit Windows (cross: -DCMAKE_TOOLCHAIN_FILE=toolchains/win64.cmake) +# +# Usage: +# Native Linux 64-bit build: +# cmake -B build-linux64 -DCMAKE_TOOLCHAIN_FILE=toolchains/linux64.cmake +# cmake --build build-linux64 +# +# Cross-compile to win64 (requires mingw-w64): +# cmake -B build-win64 -DCMAKE_TOOLCHAIN_FILE=toolchains/win64.cmake +# cmake --build build-win64 + +if(NOT DEFINED MODELICA_PLATFORM) + if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(MODELICA_PLATFORM "win64") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(MODELICA_PLATFORM "linux64") + else() + set(MODELICA_PLATFORM "unknown") + endif() +endif() + +message(STATUS "Modelica platform tag: ${MODELICA_PLATFORM}") + +set(LIBRARY_OUTPUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../Library/${MODELICA_PLATFORM}") + +# ── ExternalLib1 (static) ──────────────────────────────────────────────────── + +add_library(ExternalLib1 STATIC Func1.c) +target_include_directories(ExternalLib1 PRIVATE ../Include) + +set_target_properties(ExternalLib1 PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${LIBRARY_OUTPUT_DIR}" +) + +# ── ExternalLib2 (shared / dynamic) ───────────────────────────────────────── + +add_library(ExternalLib2 SHARED Func2.c HelperFunc.c) +target_include_directories(ExternalLib2 PRIVATE ../Include) +target_compile_definitions(ExternalLib2 PRIVATE EXTERNAL_FUNCTION_EXPORT) + +# Emit position-independent code for shared libraries on Linux +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_compile_options(ExternalLib2 PRIVATE -fPIC) +endif() + +set_target_properties(ExternalLib2 PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${LIBRARY_OUTPUT_DIR}" # .so + RUNTIME_OUTPUT_DIRECTORY "${LIBRARY_OUTPUT_DIR}" # .dll + ARCHIVE_OUTPUT_DIRECTORY "${LIBRARY_OUTPUT_DIR}" # import .lib (MSVC/MinGW) +) + +# On Windows via MinGW, also generate the import library alongside the DLL +if(MINGW) + target_link_options(ExternalLib2 PRIVATE + "-Wl,--out-implib,${LIBRARY_OUTPUT_DIR}/ExternalLib2.lib" + ) +endif() diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/Func1.c b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/Func1.c new file mode 100644 index 000000000..b8435f2c4 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/Func1.c @@ -0,0 +1,6 @@ +// File Func1.c - C source for ExternalLib1 +#include "../Include/ExternalFunc1.h" + +double ExternalFunc1_ext(double x) { + return x * x; +} diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/Func2.c b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/Func2.c new file mode 100644 index 000000000..a8b9e5b93 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/Func2.c @@ -0,0 +1,10 @@ +// File Func2.c - C source for ExternalLib2 +// EXTERNAL_FUNCTION_EXPORT is set by the build system (CMake -DEXTERNAL_FUNCTION_EXPORT) +#include "../Include/ExternalFunc2.h" + +// helperFunc is defined in HelperFunc.c, also part of ExternalLib2 +extern double helperFunc(double x); + +double ExternalFunc2(double x) { + return helperFunc(x); +} diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/HelperFunc.c b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/HelperFunc.c new file mode 100644 index 000000000..c6e322689 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/HelperFunc.c @@ -0,0 +1,4 @@ +// File HelperFunc.c - helper function included in ExternalLib2 +double helperFunc(double x) { + return 2.0 * x; +} diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/toolchains/linux64.cmake b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/toolchains/linux64.cmake new file mode 100644 index 000000000..e9a0e1f59 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/toolchains/linux64.cmake @@ -0,0 +1,6 @@ +# Toolchain: Linux 64-bit +# Native build — no extra packages required on a 64-bit Linux host. +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(MODELICA_PLATFORM "linux64" CACHE STRING "" FORCE) diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/toolchains/win64.cmake b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/toolchains/win64.cmake new file mode 100644 index 000000000..cb6ce4584 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageA/Resources/Source/toolchains/win64.cmake @@ -0,0 +1,14 @@ +# Toolchain: Windows 64-bit via MinGW-w64 +# Requires: mingw-w64 (e.g. apt install mingw-w64) +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) + +set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(MODELICA_PLATFORM "win64" CACHE STRING "" FORCE) diff --git a/RationaleMCP/0031/examples/Modelica/PackageA/package.mo b/RationaleMCP/0031/examples/Modelica/PackageA/package.mo new file mode 100644 index 000000000..a634d4058 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageA/package.mo @@ -0,0 +1,26 @@ +package PackageA + + function ExternalFunc1 "Include header file for library implementation" + input Real x; + output Real y; + external "C" + y = ExternalFunc1_ext(x) + annotation( + Library = "ExternalLib1", + Include = "#include \"ExternalFunc1.h\"", + // SourceDirectory is the default and thus redundant: + SourceDirectory = "modelica://PackageA/Resources/Source" + ); + end ExternalFunc1; + + function ExternalFunc2 "Include header file for library implementation" + input Real x; + output Real y; + external "C" + annotation( + Library = "ExternalLib2", + Include = "#include \"ExternalFunc2.h\"" + ); + end ExternalFunc2; + +end PackageA; diff --git a/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Include/ExternalFunc1.c b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Include/ExternalFunc1.c new file mode 100644 index 000000000..0d8043b60 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Include/ExternalFunc1.c @@ -0,0 +1,4 @@ +// File ExternalFunc1.c - PackageB's ExternalFunc1, included directly via annotation Include +double ExternalFunc1(double x) { + return 2.0 * x; +} diff --git a/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Library/linux64/libExternalLib1.a b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Library/linux64/libExternalLib1.a new file mode 100644 index 000000000..e87b12d82 Binary files /dev/null and b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Library/linux64/libExternalLib1.a differ diff --git a/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/.gitignore b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/.gitignore new file mode 100644 index 000000000..e0cd5b7a8 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/.gitignore @@ -0,0 +1,2 @@ +# CMake build directories +build-*/ diff --git a/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/CMakeLists.txt b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/CMakeLists.txt new file mode 100644 index 000000000..803822c0e --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.15) +project(PackageB C) + +# Determine platform tag and output directory +# Supported targets (set via toolchain file or detected automatically): +# linux64 – 64-bit Linux (native or cross: -DCMAKE_TOOLCHAIN_FILE=toolchains/linux64.cmake) +# win64 – 64-bit Windows (cross: -DCMAKE_TOOLCHAIN_FILE=toolchains/win64.cmake) +# +# Usage: +# Native Linux 64-bit build: +# cmake -B build-linux64 -DCMAKE_TOOLCHAIN_FILE=toolchains/linux64.cmake +# cmake --build build-linux64 +# +# Cross-compile to win64 (requires mingw-w64): +# cmake -B build-win64 -DCMAKE_TOOLCHAIN_FILE=toolchains/win64.cmake +# cmake --build build-win64 + +if(NOT DEFINED MODELICA_PLATFORM) + if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(MODELICA_PLATFORM "win64") + elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(MODELICA_PLATFORM "linux64") + else() + set(MODELICA_PLATFORM "unknown") + endif() +endif() + +message(STATUS "Modelica platform tag: ${MODELICA_PLATFORM}") + +set(LIBRARY_OUTPUT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../Library/${MODELICA_PLATFORM}") + +# ── ExternalLib1 (static) ─────────────────────────────────────────────────── +# Compiled from the inline C source that Modelica includes via annotation Include. +# Note: same name as PackageA's ExternalLib1 — intentional to demonstrate +# the naming conflict that requires separate LibraryDirectory paths in Base Modelica. + +add_library(ExternalLib1 STATIC ../Include/ExternalFunc1.c) + +set_target_properties(ExternalLib1 PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${LIBRARY_OUTPUT_DIR}" +) diff --git a/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/toolchains/linux64.cmake b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/toolchains/linux64.cmake new file mode 100644 index 000000000..e9a0e1f59 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/toolchains/linux64.cmake @@ -0,0 +1,6 @@ +# Toolchain: Linux 64-bit +# Native build — no extra packages required on a 64-bit Linux host. +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(MODELICA_PLATFORM "linux64" CACHE STRING "" FORCE) diff --git a/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/toolchains/win64.cmake b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/toolchains/win64.cmake new file mode 100644 index 000000000..cb6ce4584 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageB/Resources/Source/toolchains/win64.cmake @@ -0,0 +1,14 @@ +# Toolchain: Windows 64-bit via MinGW-w64 +# Requires: mingw-w64 (e.g. apt install mingw-w64) +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR x86_64) + +set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc) +set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres) + +set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32) +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(MODELICA_PLATFORM "win64" CACHE STRING "" FORCE) diff --git a/RationaleMCP/0031/examples/Modelica/PackageB/package.mo b/RationaleMCP/0031/examples/Modelica/PackageB/package.mo new file mode 100644 index 000000000..8f7fddc67 --- /dev/null +++ b/RationaleMCP/0031/examples/Modelica/PackageB/package.mo @@ -0,0 +1,12 @@ +package PackageB + + function ExternalFunc1 "Include source file" + input Real x; + output Real y; + external "C" + annotation( + Include = "#include \"ExternalFunc1.c\"" + ); + end ExternalFunc1; + +end PackageB; diff --git a/RationaleMCP/0031/examples/lower.mos b/RationaleMCP/0031/examples/lower.mos new file mode 100644 index 000000000..19520a0d5 --- /dev/null +++ b/RationaleMCP/0031/examples/lower.mos @@ -0,0 +1,16 @@ +// External Functions Example +loadFile("Modelica/PackageA/package.mo"); getErrorString(); +loadFile("Modelica/PackageB/package.mo"); getErrorString(); +loadFile("Modelica/Example.mo"); getErrorString(); + +setCommandLineOptions("--baseModelica"); getErrorString(); + +writeFile("Base Modelica/Example.bmo", OpenModelica.Scripting.instantiateModel(Example)); getErrorString(); + +// External Objects Example +loadFile("Modelica/Multiplication/package.mo"); getErrorString(); +loadFile("Modelica/ExternalObjectExample.mo"); getErrorString(); + +setCommandLineOptions("--baseModelica"); getErrorString(); + +writeFile("Base Modelica/ExternalObjectExample.bmo", OpenModelica.Scripting.instantiateModel(ExternalObjectExample)); getErrorString(); diff --git a/RationaleMCP/0031/external-function-interface.md b/RationaleMCP/0031/external-function-interface.md new file mode 100644 index 000000000..66e3f0eec --- /dev/null +++ b/RationaleMCP/0031/external-function-interface.md @@ -0,0 +1,502 @@ +# Base Modelica External Function Interface (proposal) + +> Author: Andreas Heuermann, Santa Anna IT Research, Sweden + +This proposal covers how to deal with [Modelica 12.9 External Function Interface][msl-12.9-ext-func-interface] and [Modelica 12.9.8 External Objects][msl-12.9.8-ext-obj] in Base Modelica by taking core ideas from FMU Distribution in [FMI 3.0.x][fmi-3.0.x] and ending up at a FMU like directory layout. + +## Overview + +### Rationale + +There are three distinct options to go forward with Base Modelica and external functions: + +1. Don't allow external functions in Base Modelica at all. +2. Allow external functions in Base Modelica (current status). + - Don't bundle external functions with Base Modelica. + - Bundle external functions with Base Modelica. + +Many high grade industrial Modelica libraries rely heavily on external C code. +Therefore Base Modelica needs to support external functions for any practical application for those libraries. + +One desired use case for Base Modelica is to utilize a Modelica tool's simplification and index reduction capabilities to get a reduced Base Modelica version of a Modelica model. +If that model happens to use external functions, a complete Base Modelica "package" also needs to include those external functions. +This has to be done in a way that can be automated by a tool lowering Modelica to Base Modelica. + +If a Base Modelica file that uses external functions is shared, the importing system needs to have the external functions available in some way. +This could mean installing a bunch of Modelica libraries that provided the external functions or getting a loose collection of source/binary files the importer has to save in the right locations. +This is exactly the kind of issue the [FMI standard][fmi-standard] already solved. +With a possible layered standard [FMI-LS-REF][fmi-ls-ref] it would even be possible to share the governing Modelica *and* Base Modelica files bundled nicely together with all the binaries (or sources) needed for the external functions. + +The following describes one way to bundle Base Modelica models with their used external functions without going the full [FMI-LS-REF][fmi-ls-ref] way. + +### Goals + +- Be expressive enough to convert Modelica external functions and external objects in its entirety: + - C compatibility + - Mapping of argument types from Base Modelica to target language and back + - Cover external objects +- Be as simple as possible: + - *Don't allow* arbitrary parameter order for the external function + - *Only allow* a specific location for bundled external functions + - *Only allow* binaries for external libraries + - Drop FORTRAN support + +### Related + +- Require that external functions have explicit external call ([Pull Request #3637][modelica-pull-3637]) +- Flat Modelica and External or Vendor-specific Functions ([Issue #2584][modelica-issue-2584]) + +## External Function + +### Type Mapping + +Base Modelica uses the same mapping to C as specified by [Modelica 12.9.1 Argument Type Mapping][msl-12.9.1-arg-type-mapping] and [12.9.2 Return Type Mapping][msl-12.9.2-ret-type-mapping]. + +> [!IMPORTANT] +> FORTRAN 77 is dropped for simplicity and because of the record exception. +> It can easily be added to Base Modelica or C wrappers can be used instead. + +| Modelica | C input | C output | C return type | +|----------------------|----------------------------------------|----------------------------------|-------------------| +| `Real` | `double` | `double *` | `double` | +| `Integer` | `int` | `int *` | `int` | +| `Boolean` | `int` | `int *` | `int` | +| `String` | `const char *` | `const char **` | `const char *` | +| `T[dim1]` | `const T' *, size_t dim1` | `T' *, size_t dim1` | not allowed | +| `T[dim1, dim2]` | `const T' *, size_t dim1, size_t dim2` | `T' *, size_t dim1, size_t dim2` | not allowed | +| `T[dim1, ..., dimN]` | `const T' *, ..., size_t dimN` | `T' *, ..., size_t dimN` | not allowed | +| Enumeration | `int` | `int *` | `int` | +| Record | `struct *` | `struct *` | `struct` by value | + +`Boolean` maps to `int`: `false` → 0, `true` → 1. + +An argument of the form `size(…, …)` maps to `size_t` instead. + +`String` values must be NUL-terminated; a returned `String` must be either a string input, a C string literal, or a pointer from one of the Modelica string allocation utilities. + +### Aliasing + +Keep the restriction on changing inputs in external functions. + +> An external function is not allowed to internally change the inputs (even if they are restored before the end of the function). +> +> From [Modelica 12.9.3 Aliasing][msl-12.9.3-aliasing] + +### Annotations for External Functions + +Annotations from [Modelica 12.9.4 Annotations for External Functions][msl-12.9.4-annotations] do the heavy lifting for collecting the actual external functions. + +In Base Modelica only binary distribution is supported, so `Include`, `IncludeDirectory`, and `SourceDirectory` are dropped. + +> [!NOTE] +> `Include`, `IncludeDirectory`, and `SourceDirectory` can be re-added later on + +The `modelica://` URI scheme is replaced by `base-modelica:/`, which is always relative to the directory of the current Base Modelica file, removing any dependency on package path lookups. + +The following annotations are allowed on the `external`-clause: + +| Annotation | Type | Required | Description | +|--------------------|-------------------------|----------|-------------------------------------------------------| +| `Library` | `String` or `String[:]` | yes | Library name(s) to link, without prefix or suffix | +| `LibraryDirectory` | `String` | yes | Location of the library files (`base-modelica:/` URI) | +| `License` | `String` | no | Path to the license file for the bundled library | + +Both `Library` and `LibraryDirectory` must always be present explicitly in the annotation — no defaults are provided. +Using the model name as part of the `LibraryDirectory` path (e.g. `base-modelica:/ExternalFunctions1_Example/Library`) is recommended to prevent naming conflicts between libraries from different models that happen to share a library name. +If two distinct external C libraries within the same model share a filename (e.g. both produce a shared library with the same filename), each must be placed in a separate subdirectory and annotated accordingly, e.g. `base-modelica:/ExternalFunctions1_Example/Library1/Library` and `base-modelica:/ExternalFunctions1_Example/Library2/Library`. + +The tool resolves the library by appending the platform tuple (e.g. `win64`, `linux64`, ..) as a subdirectory of `LibraryDirectory`. +The platform subdirectory is mandatory; placing libraries directly in `LibraryDirectory` is not allowed. + +```modelica +function ExternalFunc1 + input Real x; + output Real y; +external "C" + y = ExternalFunc1_ext(x) + annotation( + Library = "ExternalLib1", + LibraryDirectory = "baseModelica:/ExternalFunctions1_Example/Library", + License = "baseModelica:/ExternalFunctions1_Example/licenses/ExternalLib1.txt" + ); +end ExternalFunc1; +``` + +The files on disk next to the `.bmo` file: + +```text +ExternalFunctions1.Example.bmo +ExternalFunctions1_Example/ +├── Library/ +│ ├── x86_64-linux/ +│ │ ├── libExternalLib1.a +│ │ └── libExternalLib2.so +│ └── x86_64-windows/ +│ ├── ExternalLib1.lib +│ ├── ExternalLib2.lib +│ └── ExternalLib2.dll +└── licenses/ + ├── ExternalLib1.txt + └── ExternalLib2.txt +``` + +### Example + +Lowering of Modelica model `Example`, which uses functions from two Modelica packages. + +#### Modelica Input + +*PackageA/package.mo:* + +```modelica +package PackageA + + function ExternalFunc1 "Include header file for library implementation" + input Real x; + output Real y; + external "C" + y = ExternalFunc1_ext(x) + annotation( + Library = "ExternalLib1", + Include = "#include \"ExternalFunc1.h\"", + ); + end ExternalFunc1; + + function ExternalFunc2 "Include header file for library implementation" + input Real x; + output Real y; + external "C" + annotation( + Library = "ExternalLib2", + Include = "#include \"ExternalFunc2.h\"" + ); + end ExternalFunc2; + +end PackageA; +``` + +*PackageB/package.mo:* + +```modelica +package PackageB + + function ExternalFunc1 "Include source file" + input Real x; + output Real y; + external "C" + annotation( + Include = "#include \"ExternalFunc1.c\"" + ); + end ExternalFunc1; + +end PackageB; +``` + +*Example.mo:* + +```modelica +model Example + Real x(start = 1.0); + Real y(start = 2.0); + Real z(start = 3.0); +equation + der(x) = -PackageA.ExternalFunc1(x); + der(y) = -PackageA.ExternalFunc2(y); + der(z) = -PackageB.ExternalFunc1(z); +end Example; +``` + +Possible directory layout for `PackageA` and `PackageB`: + +```text +├── PackageA +│ ├── Resources +│ │ ├── Include +│ │ │ ├── ExternalFunc1.h +│ │ │ └── ExternalFunc2.h +│ │ ├── Library +│ │ │ ├── linux64 +│ │ │ │ ├── libExternalLib1.a +│ │ │ │ └── libExternalLib2.so +│ │ │ └── win64 +│ │ │ ├── libExternalLib1.a +│ │ │ ├── libExternalLib2.dll +│ │ │ └── libExternalLib2.dll.a +│ │ └── Source +│ │ ├── Func1.c +│ │ ├── Func2.c +│ │ └── HelperFunc.c +│ └── package.mo +└── PackageB + ├── Resources + │ ├── Include + │ │ └── ExternalFunc1.c + │ └── Library + │ └── linux64 + │ └── libExternalLib1.a + └── package.mo +``` + +#### Base Modelica Output + +The lowering tool collects all required binaries, pre-compiles any inline C sources (here `ExternalFunc3.c` becomes `libExternalFunc3`), and rewrites the annotations to use `base-modelica:/` paths. + +```modelica +//! base 0.1.0 +package 'ExternalFunctions1.Example' + function 'ExternalFunctions1.ExternalFunc1' "Include header file for library implementation" + input Real 'x'; + output Real 'y'; + external "C" 'y' = ExternalFunc1_ext('x') annotation( + Library = "ExternalLib1", + LibraryDirectory = "base-modelica:/ExternalFunctions1_Example/Library"); + end 'ExternalFunctions1.ExternalFunc1'; + + function 'ExternalFunctions1.ExternalFunc2' "Include header file for library implementation" + input Real 'x'; + output Real 'y'; + external "C" annotation( + Library = "ExternalLib2", + LibraryDirectory = "base-modelica:/ExternalFunctions1_Example/Library"); + end 'ExternalFunctions1.ExternalFunc2'; + + function 'ExternalFunctions2.ExternalFunc3' "Include source file" + input Real 'x'; + output Real 'y'; + external "C" annotation( + Library = "ExternalFunc3", + LibraryDirectory = "base-modelica:/ExternalFunctions1_Example/Library"); + end 'ExternalFunctions2.ExternalFunc3'; + + model 'Example' + Real 'x'(start = 1.0); + Real 'y'(start = 2.0); + Real 'z'(start = 3.0); + equation + der('x') = -'ExternalFunctions1.ExternalFunc1'('x'); + der('y') = -'ExternalFunctions1.ExternalFunc2'('y'); + der('z') = -'ExternalFunctions2.ExternalFunc3'('z'); + end 'Example'; +end 'ExternalFunctions1.Example'; +``` + +Files on disk next to `Example.bmo`: + +```text +├── Example +│ ├── PackageA +│ │ └── Library +│ │ └── linux64 +│ │ ├── libExternalLib1.a +│ │ └── libExternalLib2.so +│ └── PackageB +│ └── Library +│ └── linux64 +│ └── libExternalLib1.a +└── Example.bmo +``` + +## External Objects + +With a general framework for external functions established, it is also possible to discuss how to handle the special constructor and destructor functions from external objects. + +> [!IMPORTANT] +> It isn't strictly needed to limit to the new `baseModelica` URI, but it removes all guess work for finding the correct library that needs to accompany a Base Modelica file when sharing it with others. + +The author sees two plausible ways: + + 1. Re-add `class` + 2. Prefix constructor and destructor functions with the external object class specifier. + - Rules about return types of external functions need to be relaxed for the constructor. + - Rules about input types for external functions need to be relaxed for the destructor. + - Explicitly add constructor calls (easy) + - Explicitly add destructor call (needs something new) + +While the first option re-adds a single existing keyword from the Modelica language to Base Modelica, the second option would require broader changes. + +### External Object Example + +Here is a simple external object `Multiplier` from a package called `Multiplication` that needs to save some data during its lifetime. + +```modelica +model ExternalObjectExample "Multiply a decaying signal by a constant factor stored in an external object" + // The external object is created once with factor = 3.0. + // It lives on the C heap for the duration of the simulation. + Multiplication.Multiplier m = Multiplication.Multiplier(factor = 3.0); + + Real x(start = 1.0) "Decaying signal"; + Real y "Scaled output: y = 3 * x"; +equation + der(x) = -x; + y = Multiplication.multiply(m, x); +end ExternalObjectExample; +``` + +```C +/* Multiplier.c – external object that stores and applies a multiplication factor */ +#include +#include "Multiplier.h" + +typedef struct { + double factor; +} Multiplier; + +void *Multiplier_constructor(double factor) { + Multiplier *m = (Multiplier *)malloc(sizeof(Multiplier)); + m->factor = factor; + return (void *)m; +} + +void Multiplier_destructor(void *obj) { + free(obj); +} + +double Multiplier_multiply(void *obj, double x) { + Multiplier *m = (Multiplier *)obj; + return m->factor * x; +} +``` + +### Option 1: Re-add Class `class` + +The handling of the special `constructor` and `destructor` functions is one-to-one with Modelica. +No changes are needed. + +An importing tool can deduce which objects need to be allocated and freed, and when to do so. + +#### Re-adding `class` Example + +OpenModelica already produces output very similar to the one listed here. +The only difference is that it is not using the new `base-modelica` URI from this proposal. + +```modelica +loadFile("Modelica/Multiplication/package.mo"); getErrorString(); +loadFile("Modelica/ExternalObjectExample.mo"); getErrorString(); + +setCommandLineOptions("--baseModelica"); getErrorString(); + +writeFile("BaseModelica/ExternalObjectExample.bmo", OpenModelica.Scripting.instantiateModel(ExternalObjectExample)); getErrorString(); +``` + +```bash +cd examples/ +omc lower.mos +``` + +The following is a slightly updated version with `base-modelica:/` URIs and an explicitly named library directory. + +```modelica +//! base 0.1.0 +package 'ExternalObjectExample' + function 'Multiplication.multiply' "Multiply x by the factor stored in the external object" + input 'Multiplication.Multiplier' 'm'; + input Real 'x'; + output Real 'y'; + external "C" 'y' = Multiplier_multiply('m', 'x') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end 'Multiplication.multiply'; + + class 'Multiplication.Multiplier' + extends ExternalObject; + + function constructor "Allocate and initialise the multiplier" + input Real 'factor'; + output 'Multiplication.Multiplier' 'obj'; + external "C" 'obj' = Multiplier_constructor('factor') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end constructor; + + function destructor "Free the multiplier" + input 'Multiplication.Multiplier' 'obj'; + external "C" Multiplier_destructor('obj') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end destructor; + end 'Multiplication.Multiplier'; + + model 'ExternalObjectExample' "Multiply a decaying signal by a constant factor stored in an external object" + 'Multiplication.Multiplier' 'm' = 'Multiplication.Multiplier'(3.0); + Real 'x'(start = 1.0) "Decaying signal"; + Real 'y' "Scaled output: y = 3 * x"; + equation + der('x') = -'x'; + 'y' = 'Multiplication.multiply'('m', 'x'); + end 'ExternalObjectExample'; +end 'ExternalObjectExample'; +``` + +### Option 2: `constructor` / `destructor` are special external functions + +Use the class specifier of the external object `Multiplication.Multiplier` to prefix `constructor` / `destructor` functions. +Currently the output type of `'Multiplication.Multiplier.constructor'` and the input type of `'Multiplication.Multiplier.destructor'` need to be rejected since `void*` cannot be mapped to a Base Modelica type. +Relaxing that restriction without opening up all C types to be allowed in external functions could be difficult to achieve through grammar rules alone. + +Should the importing tool know the new `'Multiplication.Multiplier.constructor'` and `'Multiplication.Multiplier.destructor'` are related to an external object because the name ends in `constructor` / `destructor`? +In that case, `constructor` and `destructor` need to be reserved keywords in Base Modelica. + +Or where should a model explicitly call the constructor and destructor? +For the constructor this is easy, but what to do with the destructor? +This also does not feel very Modelica-like. +Memory management is done by the tool, not the modeler. + +#### Explicit `constructor` / `destructor` calls Example + +A possible different Base Modelica output with explicit `constructor` / `destructor` calls. +No tool prototype exists. +For demonstration purposes a new keyword `terminate` was added that becomes true after the simulation is done. + +```modelica +//! base 0.1.0 +package 'ExternalObjectExample' + function 'Multiplication.multiply' "Multiply x by the factor stored in the external object" + input 'Multiplication.Multiplier' 'm'; + input Real 'x'; + output Real 'y'; + external "C" 'y' = Multiplier_multiply('m', 'x') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end 'Multiplication.multiply'; + + function 'Multiplication.Multiplier.constructor' "Allocate and initialise the multiplier" + input Real 'factor'; + output 'Multiplication.Multiplier' 'obj'; + external "C" 'obj' = Multiplier_constructor('factor') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end 'Multiplication.Multiplier.constructor'; + + function 'Multiplication.Multiplier.destructor' "Free the multiplier" + input 'Multiplication.Multiplier' 'obj'; + external "C" Multiplier_destructor('obj') annotation( + Library = "Multiplication", + LibraryDirectory = "base-modelica:/ExternalObjectExample/Multiplication/Library"); + end 'Multiplication.Multiplier.destructor'; + + model 'ExternalObjectExample' "Multiply a decaying signal by a constant factor stored in an external object" + 'Multiplication.Multiplier' 'm' = 'Multiplication.Multiplier.constructor'(3.0); + Real 'x'(start = 1.0) "Decaying signal"; + Real 'y' "Scaled output: y = 3 * x"; + equation + der('x') = -'x'; + 'y' = 'Multiplication.multiply'('m', 'x'); + + when terminate then // <-- New construct `terminate` + 'Multiplication.Multiplier.destructor'('m'); + end when; + end 'ExternalObjectExample'; +end 'ExternalObjectExample'; +``` + +[fmi-standard]: https://fmi-standard.org +[fmi-3.0.x]: https://fmi-standard.org/docs/3.0.2 +[fmi-ls-ref]: https://github.com/modelica/fmi-ls-ref +[modelica-pull-3637]: https://github.com/modelica/ModelicaSpecification/pull/3637 +[modelica-issue-2584]: https://github.com/modelica/ModelicaSpecification/issues/2584 +[msl-12.9-ext-func-interface]: https://specification.modelica.org/master/functions.html#external-function-interface +[msl-12.9.1-arg-type-mapping]: https://specification.modelica.org/master/functions.html#argument-type-mapping +[msl-12.9.2-ret-type-mapping]: https://specification.modelica.org/master/functions.html#return-type-mapping +[msl-12.9.3-aliasing]: https://specification.modelica.org/master/functions.html#aliasing +[msl-12.9.4-annotations]: https://specification.modelica.org/master/functions.html#annotations-for-external-libraries-and-include-files +[msl-12.9.8-ext-obj]: https://specification.modelica.org/master/functions.html#external-objects diff --git a/RationaleMCP/0031/grammar.md b/RationaleMCP/0031/grammar.md index 069fddf8c..bd4fd71df 100644 --- a/RationaleMCP/0031/grammar.md +++ b/RationaleMCP/0031/grammar.md @@ -100,7 +100,7 @@ Note that there is no optional byte order mark, in agreement with the use of byt >   | **type**\ >   | ~~**operator**?~~ **record**\ >   | ( ( **pure** **constant**? ) | **impure** )? ~~**operator**?~~ **function**\ ->   ~~| **class**~~\ +>   | **class**\ >   ~~| **model**~~\ >   ~~| **block**~~\ >   ~~| **expandable**? **connector**~~\ @@ -138,15 +138,27 @@ Note that there is no optional byte order mark, in agreement with the use of byt >   ~~| **protected** (_generic-element_ **;**)*~~ \ >   )* \ >   ( _decoration_? **external** _language-specification_?\ ->    _external-function-call_? _annotation-comment_? **;**\ +>    _external-function-call_? _external-annotation-comment_? **;**\ >   )?\ >   _base-partition_* \ >   ( _annotation-comment_ **;** )? -> _language-specification_ → _STRING_ +> _language-specification_ → ~~_STRING_~~ **"C"** > _external-function-call_ → ( _component-reference_ **=** )? _IDENT_ `[(]` _expression-list_? `[)]` +> _external-annotation-comment_ → **annotation** `[(]` _external-annotation-list_ `[)]` + +> _external-annotation-list_ → _external-annotation_ ( **,** _external-annotation_ )* + +> _external-annotation_ →\ +>   **Library** **=** ( _STRING_ | `[[]` _STRING_ ( **,** _STRING_ )* `[]]` )\ +>   | **LibraryDirectory** **=** _STRING_\ +>   | **License** **=** _STRING_\ +>   ~~| **Include** **=** _STRING_~~\ +>   ~~| **IncludeDirectory** **=** _STRING_~~\ +>   ~~| **SourceDirectory** **=** _STRING_~~ + > _generic-element_ → ~~_import-clause_ | _extends-clause_ |~~ _normal-element_ | _parameter-equation_ > _normal-element_ →\