diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3d2100d..2f0dc94 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,9 +18,15 @@ jobs: - os: windows-latest version: '1.10' lib_subdir: 'x86_64-mingw32' + shim_ext: '.dll' - os: ubuntu-latest version: '1.10' lib_subdir: 'x86_64-linux-gnu' + shim_ext: '.so' + - os: macos-latest + version: '1.10' + lib_subdir: 'aarch64-apple-darwin' + shim_ext: '.dylib' steps: - uses: actions/checkout@v4 @@ -46,6 +52,32 @@ jobs: Pkg.build() ' + - name: Build ModelicaCallbacks shim (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + gcc -shared -fPIC -o lib/ext/shared/${{ matrix.lib_subdir }}/libModelicaCallbacks.so src/modelica_callbacks.c -ldl + + - name: Build ModelicaCallbacks shim (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + mkdir -p lib/ext/shared/${{ matrix.lib_subdir }} + cc -dynamiclib -fPIC -o lib/ext/shared/${{ matrix.lib_subdir }}/libModelicaCallbacks.dylib src/modelica_callbacks.c -ldl + + - name: Build ModelicaCallbacks shim (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + mkdir -p lib/ext/shared/${{ matrix.lib_subdir }} + gcc -shared -o lib/ext/shared/${{ matrix.lib_subdir }}/libModelicaCallbacks.dll src/modelica_callbacks.c -lpsapi + + - name: Upload shim artifact + uses: actions/upload-artifact@v4 + with: + name: libModelicaCallbacks-${{ matrix.lib_subdir }} + path: lib/ext/shared/${{ matrix.lib_subdir }}/libModelicaCallbacks${{ matrix.shim_ext }} + - name: Run tests (Linux) if: runner.os == 'Linux' shell: bash @@ -56,6 +88,16 @@ jobs: Pkg.test() ' + - name: Run tests (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + export DYLD_LIBRARY_PATH="$(pwd)/lib/ext/shared/${{ matrix.lib_subdir }}:${DYLD_LIBRARY_PATH}" + julia --color=yes --project -e ' + import Pkg + Pkg.test() + ' + - name: Run tests (Windows) if: runner.os == 'Windows' shell: bash @@ -66,3 +108,35 @@ jobs: import Pkg Pkg.test() ' + + release: + name: Release dynamic libraries + needs: test + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - name: Download all shim artifacts + uses: actions/download-artifact@v4 + with: + pattern: libModelicaCallbacks-* + path: artifacts + + - name: Package per platform + shell: bash + run: | + for dir in artifacts/*/; do + platform=$(basename "$dir" | sed 's/^libModelicaCallbacks-//') + cd "$dir" + tar -czvf "../../${platform}-callbacks.tar.gz" * + cd ../.. + done + + - name: Create release + uses: marvinpinto/action-automatic-releases@latest + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + draft: false + prerelease: false + title: "ModelicaCallbacks Shim ${{ github.ref_name }}" + files: | + *-callbacks.tar.gz diff --git a/Project.toml b/Project.toml index 50a0229..5afe0e7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "OMRuntimeExternalC" uuid = "ada38df0-e20e-11ed-02f3-2b4b19c1ec8a" authors = ["Adrian Pop ", "John Tinnerholm "] -version = "0.1.0" +version = "0.2.0" [deps] CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193" diff --git a/deps/build.jl b/deps/build.jl index db7c79d..44285dd 100644 --- a/deps/build.jl +++ b/deps/build.jl @@ -2,29 +2,29 @@ using HTTP import ZipFile +import Tar +import Inflate const DEPS_DIR = @__DIR__ const PACKAGE_DIR = dirname(DEPS_DIR) const PATH_TO_EXT = joinpath(PACKAGE_DIR, "lib", "ext") const RELEASE_BASE_URL = "https://github.com/OpenModelica/OMRuntimeExternalC.jl/releases/download/libs-v0.1.0" +const CALLBACKS_VERSION = "v0.1.0" +const CALLBACKS_BASE_URL = "https://github.com/OpenModelica/OMRuntimeExternalC.jl/releases/download/$(CALLBACKS_VERSION)" function downloadAndExtractLibraries(libraryString; URL) @info "Downloading archive from $(URL)..." - - #= Ensure the lib/ext directory exists =# mkpath(PATH_TO_EXT) local zipPath = joinpath(PATH_TO_EXT, libraryString * ".zip") HTTP.download(URL, zipPath) - #= Create shared directory if it does not exist =# local sharedDir = joinpath(PATH_TO_EXT, "shared") mkpath(sharedDir) @info "Extracting to $(sharedDir)..." r = ZipFile.Reader(zipPath) for f in r.files - #= Skip directory entries =# if endswith(f.name, "/") continue end @@ -35,24 +35,79 @@ function downloadAndExtractLibraries(libraryString; URL) write(outPath, read(f)) end close(r) - - #= Clean up the zip file =# rm(zipPath) @info "Successfully extracted libraries to $(sharedDir)/$(libraryString)/" end +function downloadCallbacksShim(libSubdir::String) + local url = "$(CALLBACKS_BASE_URL)/$(libSubdir)-callbacks.tar.gz" + local outDir = joinpath(PATH_TO_EXT, "shared", libSubdir) + mkpath(outDir) + + local ext = Sys.iswindows() ? ".dll" : Sys.isapple() ? ".dylib" : ".so" + local outFile = joinpath(outDir, "libModelicaCallbacks$ext") + if isfile(outFile) + @info "ModelicaCallbacks shim already exists at $outFile" + return + end + + @info "Downloading ModelicaCallbacks shim from $(url)..." + try + local tgzPath = joinpath(PATH_TO_EXT, "$(libSubdir)-callbacks.tar.gz") + HTTP.download(url, tgzPath) + open(tgzPath) do io + Tar.extract(Inflate.inflate_gzip(io), outDir) + end + rm(tgzPath) + @info "Successfully installed ModelicaCallbacks shim to $outDir" + catch e + @warn "Failed to download pre-built ModelicaCallbacks shim: $e" + @info "Attempting to compile from source..." + buildModelicaCallbacksFromSource(libSubdir) + end +end + +function buildModelicaCallbacksFromSource(libSubdir::String) + local srcFile = joinpath(PACKAGE_DIR, "src", "modelica_callbacks.c") + local outDir = joinpath(PATH_TO_EXT, "shared", libSubdir) + mkpath(outDir) + + local ext, compileCmd + if Sys.islinux() + ext = ".so" + compileCmd = `gcc -shared -fPIC -o $(joinpath(outDir, "libModelicaCallbacks$ext")) $srcFile -ldl` + elseif Sys.isapple() + ext = ".dylib" + compileCmd = `cc -dynamiclib -fPIC -o $(joinpath(outDir, "libModelicaCallbacks$ext")) $srcFile -ldl` + elseif Sys.iswindows() + ext = ".dll" + compileCmd = `gcc -shared -o $(joinpath(outDir, "libModelicaCallbacks$ext")) $srcFile` + else + @warn "Cannot build ModelicaCallbacks: unsupported platform" + return + end + + try + run(compileCmd) + @info "Successfully compiled ModelicaCallbacks shim" + catch e + @warn "Failed to compile ModelicaCallbacks shim: $e" + end +end + @static if Sys.iswindows() downloadAndExtractLibraries("x86_64-mingw32"; URL="$(RELEASE_BASE_URL)/x86_64-mingw32.zip") + downloadCallbacksShim("x86_64-mingw32") elseif Sys.islinux() downloadAndExtractLibraries("x86_64-linux-gnu"; URL="$(RELEASE_BASE_URL)/x86_64-linux-gnu.zip") + downloadCallbacksShim("x86_64-linux-gnu") elseif Sys.isapple() - @warn "macOS is currently not supported due to dylib path issues in the OpenModelica build system." - @warn "See: https://trac.openmodelica.org/OpenModelica/ticket/4647" - @warn "Some functionality requiring external C libraries will not be available." + @warn "macOS: Modelica external C libraries are not yet available." + local arch = Sys.ARCH == :aarch64 ? "aarch64-apple-darwin" : "x86_64-apple-darwin" + downloadCallbacksShim(arch) else @warn "This platform is not supported." - @warn "Some functionality requiring external C libraries will not be available." end diff --git a/src/OMRuntimeExternalC.jl b/src/OMRuntimeExternalC.jl index e5a9bf0..d9f9816 100644 --- a/src/OMRuntimeExternalC.jl +++ b/src/OMRuntimeExternalC.jl @@ -49,6 +49,19 @@ function __init__() ENV["LD_LIBRARY_PATH"] = isempty(ldpath) ? libdir : libdir * ":" * ldpath end end + #= Load the Julia-compatible ModelicaCallbacks shim FIRST with RTLD_GLOBAL + so it overrides the OMC setjmp/longjmp-based error handlers. + Then load the other libraries with RTLD_GLOBAL so their symbols are + visible to dlsym (used by the safe_* wrappers in the callbacks shim). =# + if installedLibPathlibModelicaCallbacks !== nothing + Libdl.dlopen(installedLibPathlibModelicaCallbacks, Libdl.RTLD_GLOBAL) + end + if installedLibPathlibModelicaIO !== nothing + Libdl.dlopen(installedLibPathlibModelicaIO, Libdl.RTLD_GLOBAL) + end + if installedLibPathlibModelicaExternalC !== nothing + Libdl.dlopen(installedLibPathlibModelicaExternalC, Libdl.RTLD_GLOBAL) + end catch @warn "Failed to setup the environment correctly. Make sure that you have the correct shared libraries installed." @warn "NOTE: If your Modelica model uses certain external functions your simulation might fail." diff --git a/src/api.jl b/src/api.jl index 07a9a56..33c69da 100644 --- a/src/api.jl +++ b/src/api.jl @@ -1,5 +1,32 @@ #= Add functions for the Modelica External C API here. =# +#= ---- Safe ccall error handling via ModelicaCallbacks shim ---- =# +#= + The C shim (libModelicaCallbacks.so) provides: + - Replacement ModelicaFormatError/ModelicaError that use setjmp/longjmp + instead of the OMC runtime's uninitialized thread-local jump buffers. + - safe_* wrapper functions that do setjmp entirely in C, call the target + function via dlsym, and return 0 on success or nonzero on error. + Error message is retrieved via modelica_get_error_msg(). + This avoids longjmp across Julia ccall boundaries (which is undefined behavior). +=# + +""" + get_modelica_error()::String + +Retrieve the error message from the last failed safe_* call. +""" +function get_modelica_error()::String + if installedLibPathlibModelicaCallbacks === nothing + return "ModelicaCallbacks shim not loaded" + end + ptr = ccall( + (:modelica_get_error_msg, installedLibPathlibModelicaCallbacks), + Cstring, (), + ) + return ptr == C_NULL ? "" : unsafe_string(ptr) +end + const ExternalCombiTable1D = Ptr{Nothing} """ @@ -950,3 +977,176 @@ function ModelicaStrings_hashString(string::String) string) return Int64(res) end + +#= ---- ModelicaIO functions (via safe_* wrappers in libModelicaCallbacks) ---- =# + +""" + ModelicaIO_readMatrixSizes(fileName, matrixName) -> Vector{Int64} + +Read the dimensions [nrow, ncol] of a matrix stored in a MATLAB MAT file. +""" +function ModelicaIO_readMatrixSizes(fileName::String, matrixName::String)::Vector{Int64} + dim = zeros(Cint, 2) + rc = ccall( + (:safe_ModelicaIO_readMatrixSizes, installedLibPathlibModelicaCallbacks), + Cint, + (Cstring, Cstring, Ptr{Cint}), + fileName, matrixName, dim, + ) + rc != 0 && error("ModelicaIO_readMatrixSizes failed: ", get_modelica_error()) + return Int64[dim[1], dim[2]] +end + +""" + ModelicaIO_readRealMatrix(fileName, matrixName, nrow, ncol, verbose) -> Matrix{Float64} + +Read a real matrix of size nrow x ncol from a MATLAB MAT file. +""" +function ModelicaIO_readRealMatrix( + fileName::String, + matrixName::String, + nrow::Int64, + ncol::Int64, + verbose::Bool = true, +)::Matrix{Float64} + buffer = zeros(Cdouble, nrow * ncol) + rc = ccall( + (:safe_ModelicaIO_readRealMatrix, installedLibPathlibModelicaCallbacks), + Cint, + (Cstring, Cstring, Ptr{Cdouble}, Csize_t, Csize_t, Cint), + fileName, matrixName, buffer, Csize_t(nrow), Csize_t(ncol), Cint(verbose), + ) + rc != 0 && error("ModelicaIO_readRealMatrix failed: ", get_modelica_error()) + #= C fills row-major; reshape as (ncol, nrow) then transpose for Julia column-major =# + return Matrix{Float64}(reshape(buffer, ncol, nrow)') +end + +""" + ModelicaIO_writeRealMatrix(fileName, matrixName, matrix; append, version) + +Write a real matrix to a MATLAB MAT file. +""" +function ModelicaIO_writeRealMatrix( + fileName::String, + matrixName::String, + matrix::Matrix{Float64}, + append::Bool = false, + version::String = "4", +)::Int64 + local nrow = size(matrix, 1) + local ncol = size(matrix, 2) + #= Convert Julia column-major to C row-major: transpose then flatten =# + local buffer = vec(Matrix{Float64}(matrix')) + rc = ccall( + (:safe_ModelicaIO_writeRealMatrix, installedLibPathlibModelicaCallbacks), + Cint, + (Cstring, Cstring, Ptr{Cdouble}, Csize_t, Csize_t, Cint, Cstring), + fileName, matrixName, buffer, Csize_t(nrow), Csize_t(ncol), Cint(append), version, + ) + rc == -1 && error("ModelicaIO_writeRealMatrix failed: ", get_modelica_error()) + return Int64(rc) +end + +#= ---- ModelicaInternal functions (via safe_* wrappers in libModelicaCallbacks) ---- =# + +""" + ModelicaInternal_print(string, fileName) + +Print a string to a file (append mode) or to stdout if fileName is empty. +""" +function ModelicaInternal_print(string::String, fileName::String) + rc = ccall( + (:safe_ModelicaInternal_print, installedLibPathlibModelicaCallbacks), + Cint, + (Cstring, Cstring), + string, fileName, + ) + rc != 0 && error("ModelicaInternal_print failed: ", get_modelica_error()) + return nothing +end + +""" + ModelicaInternal_readLine(fileName, lineNumber) -> (String, Bool) + +Read a specific line from a file. Returns (line_content, endOfFile). +""" +function ModelicaInternal_readLine(fileName::String, lineNumber::Int64) + bufPtr = Ref{Cstring}(C_NULL) + endOfFile = Ref{Cint}(0) + rc = ccall( + (:safe_ModelicaInternal_readLine, installedLibPathlibModelicaCallbacks), + Cint, + (Cstring, Cint, Ref{Cstring}, Ref{Cint}), + fileName, Cint(lineNumber), bufPtr, endOfFile, + ) + rc != 0 && error("ModelicaInternal_readLine failed: ", get_modelica_error()) + local str = bufPtr[] == C_NULL ? "" : unsafe_string(bufPtr[]) + return (str, endOfFile[] != 0) +end + +""" + ModelicaInternal_countLines(fileName) -> Int64 + +Count the number of lines in a file. +""" +function ModelicaInternal_countLines(fileName::String)::Int64 + result = Ref{Cint}(0) + rc = ccall( + (:safe_ModelicaInternal_countLines, installedLibPathlibModelicaCallbacks), + Cint, + (Cstring, Ref{Cint}), + fileName, result, + ) + rc != 0 && error("ModelicaInternal_countLines failed: ", get_modelica_error()) + return Int64(result[]) +end + +""" + ModelicaInternal_fullPathName(fileName) -> String + +Return the full (absolute) path name of a file. +""" +function ModelicaInternal_fullPathName(fileName::String)::String + result = Ref{Cstring}(C_NULL) + rc = ccall( + (:safe_ModelicaInternal_fullPathName, installedLibPathlibModelicaCallbacks), + Cint, + (Cstring, Ref{Cstring}), + fileName, result, + ) + rc != 0 && error("ModelicaInternal_fullPathName failed: ", get_modelica_error()) + return result[] == C_NULL ? "" : unsafe_string(result[]) +end + +""" + ModelicaInternal_stat(name) -> Int64 + +Return Modelica FileType: 1=NoFile, 2=RegularFile, 3=Directory, 4=SpecialFile. +""" +function ModelicaInternal_stat(name::String)::Int64 + result = Ref{Cint}(0) + rc = ccall( + (:safe_ModelicaInternal_stat, installedLibPathlibModelicaCallbacks), + Cint, + (Cstring, Ref{Cint}), + name, result, + ) + rc != 0 && error("ModelicaInternal_stat failed: ", get_modelica_error()) + return Int64(result[]) +end + +""" + ModelicaStreams_closeFile(fileName) + +Close a file that was opened for reading via ModelicaInternal_readLine. +""" +function ModelicaStreams_closeFile(fileName::String) + rc = ccall( + (:safe_ModelicaStreams_closeFile, installedLibPathlibModelicaCallbacks), + Cint, + (Cstring,), + fileName, + ) + rc != 0 && error("ModelicaStreams_closeFile failed: ", get_modelica_error()) + return nothing +end diff --git a/src/modelica_callbacks.c b/src/modelica_callbacks.c new file mode 100644 index 0000000..a33efb8 --- /dev/null +++ b/src/modelica_callbacks.c @@ -0,0 +1,316 @@ +/* + * Julia-compatible implementations of ModelicaUtilities callback functions, + * plus safe wrappers for ModelicaIO/ModelicaExternalC functions. + * + * The Modelica specification requires the simulation environment to provide + * ModelicaError, ModelicaFormatError, etc. The OpenModelica runtime provides + * these using setjmp/longjmp with thread-local data initialized by OMC. + * When calling from Julia, that data is not set up, causing segfaults. + * + * This shim: + * 1) Replaces the OMC error handlers with our own (load with RTLD_GLOBAL) + * 2) Provides safe_* wrapper functions that do setjmp in C, call the target + * via dlsym, and return an error code + message on failure. + * + * Compile: gcc -shared -fPIC -o libModelicaCallbacks.so modelica_callbacks.c -ldl + */ + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +# include +# include +static void* win_find_symbol(const char* name) { + HANDLE process = GetCurrentProcess(); + HMODULE modules[512]; + DWORD needed = 0; + if (!EnumProcessModules(process, modules, sizeof(modules), &needed)) + return NULL; + DWORD count = needed / sizeof(HMODULE); + for (DWORD i = 0; i < count; i++) { + FARPROC addr = GetProcAddress(modules[i], name); + if (addr) return (void*)addr; + } + return NULL; +} +# define DLSYM_DEFAULT(name) win_find_symbol(name) +#else +# include +# define DLSYM_DEFAULT(name) dlsym(RTLD_DEFAULT, name) +#endif + +/* Thread-local jump buffer and error message storage */ +static __thread jmp_buf *jmpbuf_ptr = NULL; +static __thread char error_msg[4096]; + +/* Returns the stored error message (called from Julia after a safe_* call fails) */ +const char* modelica_get_error_msg(void) { + return error_msg; +} + +/* ---- ModelicaUtilities callback replacements ---- */ + +void ModelicaFormatError(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vsnprintf(error_msg, sizeof(error_msg), fmt, ap); + va_end(ap); + if (jmpbuf_ptr) { + longjmp(*jmpbuf_ptr, 1); + } + fprintf(stderr, "ModelicaFormatError: %s\n", error_msg); + abort(); +} + +void ModelicaVFormatError(const char *fmt, va_list ap) { + vsnprintf(error_msg, sizeof(error_msg), fmt, ap); + if (jmpbuf_ptr) { + longjmp(*jmpbuf_ptr, 1); + } + fprintf(stderr, "ModelicaVFormatError: %s\n", error_msg); + abort(); +} + +void ModelicaError(const char *msg) { + strncpy(error_msg, msg, sizeof(error_msg) - 1); + error_msg[sizeof(error_msg) - 1] = '\0'; + if (jmpbuf_ptr) { + longjmp(*jmpbuf_ptr, 1); + } + fprintf(stderr, "ModelicaError: %s\n", error_msg); + abort(); +} + +void ModelicaFormatMessage(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vfprintf(stdout, fmt, ap); + va_end(ap); +} + +void ModelicaVFormatMessage(const char *fmt, va_list ap) { + vfprintf(stdout, fmt, ap); +} + +void ModelicaMessage(const char *msg) { + fputs(msg, stdout); +} + +void ModelicaFormatWarning(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + fprintf(stderr, "ModelicaWarning: "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + va_end(ap); +} + +void ModelicaVFormatWarning(const char *fmt, va_list ap) { + fprintf(stderr, "ModelicaWarning: "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); +} + +void ModelicaWarning(const char *msg) { + fprintf(stderr, "ModelicaWarning: %s\n", msg); +} + +/* Memory allocation callbacks */ +char* ModelicaAllocateString(size_t len) { + char *s = (char *)malloc(len + 1); + if (s) s[0] = '\0'; + return s; +} + +char* ModelicaAllocateStringWithErrorReturn(size_t len) { + return (char *)malloc(len + 1); +} + +/* ---- Safe wrappers: setjmp in C, call target via dlsym ---- */ +/* Return 0 on success, 1 on error (message in modelica_get_error_msg()) */ + +int safe_ModelicaIO_readMatrixSizes( + const char* fileName, const char* matrixName, int* dim) +{ + typedef void (*fn_t)(const char*, const char*, int*); + fn_t fn = (fn_t)DLSYM_DEFAULT("ModelicaIO_readMatrixSizes"); + if (!fn) { + snprintf(error_msg, sizeof(error_msg), "ModelicaIO_readMatrixSizes not found"); + return 1; + } + jmp_buf buf; + jmpbuf_ptr = &buf; + if (setjmp(buf) == 0) { + fn(fileName, matrixName, dim); + jmpbuf_ptr = NULL; + return 0; + } else { + jmpbuf_ptr = NULL; + return 1; + } +} + +int safe_ModelicaIO_readRealMatrix( + const char* fileName, const char* matrixName, + double* matrix, size_t m, size_t n, int verbose) +{ + typedef void (*fn_t)(const char*, const char*, double*, size_t, size_t, int); + fn_t fn = (fn_t)DLSYM_DEFAULT("ModelicaIO_readRealMatrix"); + if (!fn) { + snprintf(error_msg, sizeof(error_msg), "ModelicaIO_readRealMatrix not found"); + return 1; + } + jmp_buf buf; + jmpbuf_ptr = &buf; + if (setjmp(buf) == 0) { + fn(fileName, matrixName, matrix, m, n, verbose); + jmpbuf_ptr = NULL; + return 0; + } else { + jmpbuf_ptr = NULL; + return 1; + } +} + +int safe_ModelicaIO_writeRealMatrix( + const char* fileName, const char* matrixName, + double* matrix, size_t m, size_t n, int append, const char* version) +{ + typedef int (*fn_t)(const char*, const char*, double*, size_t, size_t, int, const char*); + fn_t fn = (fn_t)DLSYM_DEFAULT("ModelicaIO_writeRealMatrix"); + if (!fn) { + snprintf(error_msg, sizeof(error_msg), "ModelicaIO_writeRealMatrix not found"); + return -1; + } + jmp_buf buf; + jmpbuf_ptr = &buf; + if (setjmp(buf) == 0) { + int res = fn(fileName, matrixName, matrix, m, n, append, version); + jmpbuf_ptr = NULL; + return res; + } else { + jmpbuf_ptr = NULL; + return -1; + } +} + +int safe_ModelicaInternal_print(const char* string, const char* fileName) { + typedef void (*fn_t)(const char*, const char*); + fn_t fn = (fn_t)DLSYM_DEFAULT("ModelicaInternal_print"); + if (!fn) { + snprintf(error_msg, sizeof(error_msg), "ModelicaInternal_print not found"); + return 1; + } + jmp_buf buf; + jmpbuf_ptr = &buf; + if (setjmp(buf) == 0) { + fn(string, fileName); + jmpbuf_ptr = NULL; + return 0; + } else { + jmpbuf_ptr = NULL; + return 1; + } +} + +int safe_ModelicaInternal_readLine( + const char* fileName, int lineNumber, + const char** buffer, int* endOfFile) +{ + typedef const char* (*fn_t)(const char*, int, int*); + fn_t fn = (fn_t)DLSYM_DEFAULT("ModelicaInternal_readLine"); + if (!fn) { + snprintf(error_msg, sizeof(error_msg), "ModelicaInternal_readLine not found"); + return 1; + } + jmp_buf buf; + jmpbuf_ptr = &buf; + if (setjmp(buf) == 0) { + *buffer = fn(fileName, lineNumber, endOfFile); + jmpbuf_ptr = NULL; + return 0; + } else { + jmpbuf_ptr = NULL; + return 1; + } +} + +int safe_ModelicaInternal_countLines(const char* fileName, int* result) { + typedef int (*fn_t)(const char*); + fn_t fn = (fn_t)DLSYM_DEFAULT("ModelicaInternal_countLines"); + if (!fn) { + snprintf(error_msg, sizeof(error_msg), "ModelicaInternal_countLines not found"); + return 1; + } + jmp_buf buf; + jmpbuf_ptr = &buf; + if (setjmp(buf) == 0) { + *result = fn(fileName); + jmpbuf_ptr = NULL; + return 0; + } else { + jmpbuf_ptr = NULL; + return 1; + } +} + +int safe_ModelicaInternal_fullPathName(const char* fileName, const char** result) { + typedef const char* (*fn_t)(const char*); + fn_t fn = (fn_t)DLSYM_DEFAULT("ModelicaInternal_fullPathName"); + if (!fn) { + snprintf(error_msg, sizeof(error_msg), "ModelicaInternal_fullPathName not found"); + return 1; + } + jmp_buf buf; + jmpbuf_ptr = &buf; + if (setjmp(buf) == 0) { + *result = fn(fileName); + jmpbuf_ptr = NULL; + return 0; + } else { + jmpbuf_ptr = NULL; + return 1; + } +} + +int safe_ModelicaInternal_stat(const char* name, int* result) { + typedef int (*fn_t)(const char*); + fn_t fn = (fn_t)DLSYM_DEFAULT("ModelicaInternal_stat"); + if (!fn) { + snprintf(error_msg, sizeof(error_msg), "ModelicaInternal_stat not found"); + return 1; + } + jmp_buf buf; + jmpbuf_ptr = &buf; + if (setjmp(buf) == 0) { + *result = fn(name); + jmpbuf_ptr = NULL; + return 0; + } else { + jmpbuf_ptr = NULL; + return 1; + } +} + +int safe_ModelicaStreams_closeFile(const char* fileName) { + typedef void (*fn_t)(const char*); + fn_t fn = (fn_t)DLSYM_DEFAULT("ModelicaStreams_closeFile"); + if (!fn) { + snprintf(error_msg, sizeof(error_msg), "ModelicaStreams_closeFile not found"); + return 1; + } + jmp_buf buf; + jmpbuf_ptr = &buf; + if (setjmp(buf) == 0) { + fn(fileName); + jmpbuf_ptr = NULL; + return 0; + } else { + jmpbuf_ptr = NULL; + return 1; + } +} diff --git a/src/pathSetup.jl b/src/pathSetup.jl index add17a7..11b4cdf 100644 --- a/src/pathSetup.jl +++ b/src/pathSetup.jl @@ -67,3 +67,14 @@ else end libPathlibModelicaExternalC end + +const _libPathlibModelicaCallbacks = locateSharedParserLibrary(INSTALLATION_DIRECTORY_PATH, "ModelicaCallbacks", "lib") +const libPathlibModelicaCallbacks = locateSharedParserLibrary(SHARED_DIRECTORY_PATH, "ModelicaCallbacks", "shared") +const installedLibPathlibModelicaCallbacks = if _libPathlibModelicaCallbacks !== nothing + _libPathlibModelicaCallbacks +else + if libPathlibModelicaCallbacks === nothing + @warn "ModelicaCallbacks shim not found (error handling in external C functions may segfault)" + end + libPathlibModelicaCallbacks +end diff --git a/test/runtests.jl b/test/runtests.jl index 1bd4345..0ad7260 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -318,6 +318,108 @@ const ORC = OMRuntimeExternalC ORC.ModelicaStandardTables_CombiTable2D_close(tableID) end + @testset "ModelicaIO functions" begin + local testMatFile = tempname() * ".mat" + + @testset "writeRealMatrix and readMatrixSizes" begin + local mat = [1.0 2.0 3.0; 4.0 5.0 6.0] #= 2x3 matrix =# + res = ORC.ModelicaIO_writeRealMatrix(testMatFile, "testMatrix", mat) + @test res == 1 #= C function returns 1 on success =# + @test isfile(testMatFile) + + dims = ORC.ModelicaIO_readMatrixSizes(testMatFile, "testMatrix") + @test dims == [2, 3] + end + + @testset "readRealMatrix" begin + local mat = ORC.ModelicaIO_readRealMatrix(testMatFile, "testMatrix", Int64(2), Int64(3)) + @test size(mat) == (2, 3) + @test isapprox(mat[1, 1], 1.0, atol=1e-10) + @test isapprox(mat[1, 2], 2.0, atol=1e-10) + @test isapprox(mat[1, 3], 3.0, atol=1e-10) + @test isapprox(mat[2, 1], 4.0, atol=1e-10) + @test isapprox(mat[2, 2], 5.0, atol=1e-10) + @test isapprox(mat[2, 3], 6.0, atol=1e-10) + end + + @testset "writeRealMatrix separate file" begin + local f2 = tempname() * ".mat" + res = ORC.ModelicaIO_writeRealMatrix(f2, "secondMatrix", [10.0 20.0; 30.0 40.0]) + @test res == 1 #= C function returns 1 on success =# + + dims2 = ORC.ModelicaIO_readMatrixSizes(f2, "secondMatrix") + @test dims2 == [2, 2] + + mat2 = ORC.ModelicaIO_readRealMatrix(f2, "secondMatrix", Int64(2), Int64(2)) + @test isapprox(mat2[1, 1], 10.0, atol=1e-10) + @test isapprox(mat2[2, 2], 40.0, atol=1e-10) + rm(f2, force=true) + end + + @testset "roundtrip with 1x1 matrix" begin + local f = tempname() * ".mat" + ORC.ModelicaIO_writeRealMatrix(f, "scalar", [42.0;;]) + dims = ORC.ModelicaIO_readMatrixSizes(f, "scalar") + @test dims == [1, 1] + mat = ORC.ModelicaIO_readRealMatrix(f, "scalar", Int64(1), Int64(1)) + @test isapprox(mat[1, 1], 42.0, atol=1e-10) + rm(f, force=true) + end + + rm(testMatFile, force=true) + end + + @testset "ModelicaInternal functions" begin + @testset "ModelicaInternal_fullPathName" begin + p = ORC.ModelicaInternal_fullPathName(".") + @test isabspath(p) + @test isdir(p) + end + + @testset "ModelicaInternal_stat" begin + #= Modelica FileType enum: 1=NoFile, 2=RegularFile, 3=Directory, 4=SpecialFile =# + @test ORC.ModelicaInternal_stat(@__FILE__) == 2 #= RegularFile =# + @test ORC.ModelicaInternal_stat(@__DIR__) == 3 #= Directory =# + @test ORC.ModelicaInternal_stat("/nonexistent_path_12345") == 1 #= NoFile =# + end + + @testset "ModelicaInternal_print and readLine and countLines" begin + local f = tempname() + #= Write two lines using Julia IO to have a known file format =# + open(f, "w") do io + println(io, "line one") + println(io, "line two") + end + + @test ORC.ModelicaInternal_countLines(f) == 2 + + (line1, eof1) = ORC.ModelicaInternal_readLine(f, Int64(1)) + @test line1 == "line one" + @test eof1 == false + + (line2, eof2) = ORC.ModelicaInternal_readLine(f, Int64(2)) + @test line2 == "line two" + #= endOfFile is only true when reading PAST the last line =# + @test eof2 == false + + (line3, eof3) = ORC.ModelicaInternal_readLine(f, Int64(3)) + @test line3 == "" + @test eof3 == true + + ORC.ModelicaStreams_closeFile(f) + rm(f, force=true) + end + + @testset "ModelicaInternal_print to file" begin + local f = tempname() + ORC.ModelicaInternal_print("hello", f) + ORC.ModelicaStreams_closeFile(f) + @test isfile(f) + @test strip(read(f, String)) == "hello" + rm(f, force=true) + end + end + @testset "ModelicaStrings functions" begin @testset "ModelicaStrings_length" begin @test ORC.ModelicaStrings_length("") == 0