Skip to content

Provide access to ModelicaUtilities in external functions compiled as dynamically linked libraries #3771

@casella

Description

@casella

Problem statement

The Modelica Specification should allow library developers to write external functions that include calls to ModelicaUtilities functions, with their binary code provided by dynamically linked / shared libraries. Such external functions should be usable in the same contexts and with the same input variables as their regular Modelica function counterparts, to fully enable the replaceable/redeclare mechanism.

These are the detailed requirements:

  1. The interface (input and output variables) of two functions that do the same job, one implemented by a Modelica algorithm and the other one implemented as an external function, should be exactly the same. Rationale: this is required so they can be swapped by the replaceable/redeclare mechanism.
  2. The external functions should be callable both at Modelica compile time and at simulation run time. Rationale: regular Modelica functions defined by a Modelica algorithm can be evaluated at Modelica compile time, e.g. when they define the value of structural parameters or when the annotation(Evaluate = true) is set. Hence, corresponding external Modelica functions should be usable in the same contexts.
  3. External functions should be able to call functions from ModelicaUtilities. Rationale: this is required by the MLS Section 12.9.6.
  4. The binary code of external functions should be provided by one dynamically linked / shared library for each OS. Rationale: standard interfaces for .dll/.so libraries are defined by operating systems such as Windows and Unix, hence only one binary library file must be provided for each supported OS. Conversely, statically linked libraries are not guaranteed to work with versions of the C compiler other than the one that was used to compile them; this forces library developers to provide compiled binaries for each version of each compiler that can potentially be used by the final user to compile the simulation executable, constantly updating them as new compiler versions are shipped; this is a developer's nightmare. Also, calling functions in statically linked libraries at (Modelica) compile time is much less straightforward than calling functions in a .dll/.so library, e.g., via libffi.
  5. A library containing external functions compiled into dynamically linked/shared libraries that call ModelicaUtilities functions should run in all Modelica tools and under all OSs. Rationale: external functions and ModelicaUtilities are both part of the MLS, so any library built following those rules should be runnable in any Modelica tool and operating system out of the box.
  6. It should be possible to generate FMUs of Modelica models using such libraries. In this case, the behaviour of the ModelicaUtilities functions called by the external C function should be suitably mapped into the equivalent logging an error-handling mechanisms of the FMI standard.
  7. How one can write external functions compiled in a dynamically linked/shared library that access ModelicaUtilities should be clearly explained in the Language Specification and should preferably not involve writing tricky C code in every new external function. Rationale: Modelica is a high-level modelling language, writing a Modelica library which involves some external C code should not require advanced or exotic C programming skills.

Minimal working example

The following library exemplifies the above-stated requirements in a MWE.

package ExternalFunctionsModelicaUtilities
  package BaseFunctions "Common interface declarations for functions"
    function size "Returns size of arrays based on name input"
      input String name;
      output Integer N;
    end size;
    
    function compute "Compute a function of x based on the name input"
      input String name;
      input Real x;
      output Real y;
    end compute;
  end BaseFunctions;

  package ModelicaFunctions "Modelica implementation"
    extends BaseFunctions;
    redeclare function extends size
    algorithm
      if name == "foobar" then
        N := 3;
      else
        assert(false, "Unsupported name");
      end if;
    end size;
    
    redeclare function extends compute
      input String name;
      input Real x;
      output Real y;
    algorithm
      if name == "foobar" then
        y := 1 + x^2;
        assert(y <= 10, "Value of x = " + String(x) + " could be too high", AssertionLevel.warning);
        assert(y <= 17, "Value of x = "+ String(x) + " is too high");
      else
        assert(false, "Unsupported name");
      end if;
    end compute;
  end ModelicaFunctions;
  
  package ExternalFunctions
    extends BaseFunctions;

    redeclare function extends size
      external "C";
    annotation(
      LibraryDirectory="modelica://ExternalFunctionsModelicaUtilities/Resources/Source/build",
      Library="functions",
      IncludeDirectory="modelica://ExternalFunctionsModelicaUtilities/Resources/Source",
      Include="#include \"functions.h\"");
    end size;
    
    redeclare function extends compute
      external "C";
    annotation(
      LibraryDirectory="modelica://ExternalFunctionsModelicaUtilities/Resources/Source/build",
      Library="functions",
      IncludeDirectory="modelica://ExternalFunctionsModelicaUtilities/Resources/Source",
      Include="#include \"functions.h\"");
    end compute;
  end ExternalFunctions;

  partial model M "The mother of all test models"
    replaceable package P = BaseFunctions;
    parameter String name = "undefined";
    parameter Integer N = P.size(name) annotation(Evaluate = true);
    Real y[N];
  equation
    for i in 1:N loop
      y[i] = P.compute(name, time/i);
    end for;
  end M;
  
  model M1M "Should fail at compile time with 'Unsupported name' error"
    extends M(redeclare package P = ModelicaFunctions);
    annotation(experiment(StopTime = 10));
  end M1M;
  
  model M2M "Shoud simulate until time = 4 with warning at time = 3, then fail"
    extends M(redeclare package P = ModelicaFunctions, name = "foobar");
    annotation(experiment(StopTime = 10));
  end M2M;

  model M3M "Same as M2M but N is provided by a literal, not by a function"
    extends M(redeclare package P = ModelicaFunctions, name = "foobar", N = 3);
    annotation(experiment(StopTime = 10));
  end M3M;

  model M1E "Same as M1M with external function implementation"
    extends M1M(redeclare package P = ExternalFunctions);
    annotation(experiment(StopTime = 10));
  end M1E;
  
  model M2E "Same as M2M with external function implementation"
    extends M2M(redeclare package P = ExternalFunctions);
    annotation(experiment(StopTime = 10));
  end M2E;

  model M3E "Same as M3M with external function implementation"
    extends M3M(redeclare package P = ExternalFunctions);
    annotation(experiment(StopTime = 10));
  end M3E;
end ExternalFunctionsModelicaUtilities;
/* functions.c */
#include "functions.h"
#include <string.h>
#include <stdio.h>
#include "ModelicaUtilities.h"

int size(const char *name)
{
  if (!strcmp(name, "foobar"))
    return 3;
  else
    ModelicaError("Unsupported name");
}

double compute(const char *name, double x)
{
  double y;
  char str[100];
 
  if (!strcmp(name, "foobar"))
    {
	  y = 1 + x*x;
	  if (y > 10.0)
	  {
	    snprintf(str, 100, "Value of x = %f could be too high", x);
		ModelicaWarning(str);
	  }
	  if (y > 17.0)
	  {
	    snprintf(str, 100, "Value of x = %f is too high", x);
		ModelicaError(str);
	  }
	  return y;
	}
  else
    ModelicaError("Unsupported name");
}
/* functions.h */
#pragma once

int size(const char *name);
double compute(const char *name, double x);

The binaries can be compiled using CMAKE with the following CMakeLists.txt file:

cmake_minimum_required(VERSION 3.5)
set(CMAKE_BUILD_TYPE Release)
project(functions)
add_library(functions SHARED functions.c)

The full MWE source code is provided here. The ModelicaUtilities.h file was copied from the latest version provided by the MSL 4.1.0.

Motivation

The above-described MWE is motivated in particular (but not exclusively!) by the ExternalMedia library. The goal of this library is to provide Modelica.Media compatible medium models, which use external code (e.g. CoolProp) to compute the medium properties. Note that the Modelica.Media library provides a purely functional interface, so one can write the following generic model:

model M
  replaceable package Medium = Modelica.Media.Interfaces.PartialTwoPhaseMedium;
  Medium.Density d = Medium.density_pT(1e5, 300);
end M;

Model M can be instantiated to use either a built-in Modelica.Media medium model, whose functions are defined in by Modelica algorithms, or the ExternalMedia medium models, whose functions are defined as external functions:

model S
  M m1(redeclare package Medium = Modelica.Media.Water.StandardWater);
  M m2(redeclare package Medium = ExternalMedia.Examples.WaterCoolProp);
end M;

Of course, proper error handling via ModelicaError must be provided to correctly cope with property functions called outside their domain of validity during simulation, or to report invalid medium description strings with proper error messages.

Current status

The code shown above is valid, according to the latest MLS draft specification. When running CMAKE under Windows, using Visual Studio 2019, a linker error is issued, complaining that the ModelicaError and ModelicaWarning symbols are unresolved. When running CMAKE under Linux, the binaries are compiled successfully, because Linux defers checking the availablity of those symbols at runtime.

In fact, all the example models in the MWE package run in OpenModelica under Linux. At compile time, OMC uses the libffi library to access the external size() function and exports the ModelicaError and ModelicaWarning symbols, which are then called by the external function, triggering errors that are shown in the GUI. At simulation run time, the simulation executable loads the function.so shared library and exports the ModelicaError and ModelicaWarning symbols, so that the external functions can call them, triggering the appropriate response by the simulation executable. This can point to a possible solution strategy (see below) but currently does not fulfill requirement 5.

Bottom line, AFAIU, it is not possible to fullfill the six stated requirements with the current MLS.

Candidate solutions

At the moment, there are several possible candidate solution:

  • One is proposed by myself and @fedetftpolimi in ModelicaUtilities for shared object / dynamic link library #2191 here and here. The idea is to mandate that Modelica tools and simulation runtimes export the ModelicaUtilities symbols. This is enough to the get the MWE as shown working on Linux. On Windows, you need some tricky C code to explicitly handle the importing of the symbols; luckily this code can be written once and for all and became part of Modelica.Utilities, so it does not put too much of a burden to the library developer. This proposal fulfills the six requirements (6. is a bit stretched on Windows) and was tested successfully on OMC.
  • One is proposed by @t-sommer in #4476 here. The idea is to define a ModelicaUtilityFunction object in the (EDIT) ModelicaServices library (I guess that's the right place for it), which is implemented by each tool vendor. One can then use this external object to get the pointers to the ModelicaUtilities functions. It is not clear to me at the moment if this proposal fullfills the six requirements listed above, in particular number 2. Also, one potential issue that is pointed out by the MWE is that if you have a purely functional interface, there is no place to instantiate the external object. However, as pointed out in the current draft of Section 12.9.7: "External objects may be a protected component (or part of one) in a function. The constructor is in that case called at the start of the function call, and the destructor when the function returns, or when recovering from errors in the function.". So, one could write a "glue" function in Modelica that internally declares this external object and then passess it to the actual external function. If this works, it would be better than the previous solution from the point of view of requirement 6. I'm not sure it would fulfill requirement 2.
  • One is sketched in #4476 by @HansOlsson. It involves a statically linked "glue" C function providing the pointers to ModelicaUtilities functions to a dynamically linked function with the actual external function code. The proposed C-code of the glue external functions looks a bit tricky to me, so requirement 6. is not fully fulfilled. Also, it is not clear to me if requirement 2. can be fulfilled at all.
  • Last, but not least, ModelicaUtilities for shared object / dynamic link library #2191 reports the first proposal made by @beutlich on the old trac system. The idea is that tools provide ModelicaExternalC.so/ModelicaExternalC.dll shared libraries which export the ModelicaUtilities symbols, so they can be linked by the external functions. There is some discussion there, I'm not sure if this proposal is actually superseded by the first candidate solution in this list.

Proposed action

To me, the six requirements posted above are more than reasonable and I hope there is agreement on them. If so, we should investigate which of the proposed solutions can fulfill them and then choose the best one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions