Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e0d2fa1
Generalize Julia callback wrapper to accept scalar arguments dynamica…
dmitry-kabanov Apr 30, 2026
4c54ec8
[julia] Convert callback wrapper arguments to C-OIF compatible arguments
dmitry-kabanov Apr 30, 2026
90f178d
[julia] Add to callbacks conversion of the arguments to OIF_TYPE_USER…
dmitry-kabanov May 4, 2026
54c3587
[julia, tests] Add tests for callback.jl for arguments of types OIF_T…
dmitry-kabanov May 4, 2026
e72eb07
[julia, Bridge] Wrap C callbacks using provided OIFArgType array for …
dmitry-kabanov May 4, 2026
e4d2bab
[python, converter] Match OIFCallback to C and Julia versions
dmitry-kabanov May 4, 2026
ce8b16f
[julia] Accept separate argument in a callback wrapper instead of a t…
dmitry-kabanov May 4, 2026
4ce56b7
[julia] Handle edge cases in callback wrappers on the implementation …
dmitry-kabanov May 5, 2026
f1fc392
[julia, optim::optim_jl] Test eagerly that the objective function cal…
dmitry-kabanov May 5, 2026
35d92d2
[julia, converter] Keep hidden reference to dims member of OIFArrayF64
dmitry-kabanov May 5, 2026
2088c2b
[python, tests] Update tests for the optimization interface with scip…
dmitry-kabanov May 5, 2026
bcfe85b
[misc] Reformat source code
dmitry-kabanov May 5, 2026
3806cb3
[julia, converter] Fix memory bug introduced by adding a member to OI…
dmitry-kabanov May 7, 2026
2458b8b
[julia, converter] Remove type constraint
dmitry-kabanov May 7, 2026
5ac80d4
[misc] Reformat code
dmitry-kabanov May 7, 2026
a09992c
[misc] Remove extraneous debug message
dmitry-kabanov May 8, 2026
3aec652
Preserve constructed tuple during cfunction call
dmitry-kabanov May 8, 2026
3c0b930
[python, optim::scipy_optimize] Print all statistics
dmitry-kabanov May 8, 2026
9612c2f
[misc] Use ctest --verbose to understand error on GitHub actions
dmitry-kabanov May 8, 2026
83d388f
[julia] Adjust how OIFArrayF64 structures are passed to the C callback
dmitry-kabanov May 8, 2026
23196be
[julia] Use correct path to OpenInterfaces when inside OpenInterfaces…
dmitry-kabanov May 11, 2026
c3903de
[julia] Constrain versions of OrdinaryDiffEq
dmitry-kabanov May 11, 2026
91d0ad6
[julia, tests] Fix callback.jl tests by using function interface corr…
dmitry-kabanov May 11, 2026
30a7200
[misc] Remove Julia 1.12 from CI due to undetermined memory crashes
dmitry-kabanov May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/qa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
julia-version: ['1.11', '1.12']
julia-version: ['1.11'] # Excluding Julia 1.12 due to memory crashes , '1.12']
defaults:
run:
shell: bash -l {0}
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ help:
.PHONY : test
test :
@echo "=== C tests ==="
cd build && ctest --output-on-failure
cd build && ctest --verbose --output-on-failure
@echo "=== Julia tests ==="
julia tests/lang_julia/runtests.jl
@echo "=== Python tests ==="
Expand Down
7 changes: 7 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[deps]
DiffEqBase = "2b5f629d-d688-5b77-993f-72d75c75574e"
Ipopt = "b6b21f68-93f8-5de0-b562-5493be1d77c9"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
Expand All @@ -16,3 +17,9 @@ SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"
[sources]
OpenInterfaces = {path = "lang_julia/OpenInterfaces"}
OpenInterfacesImpl = {path = "lang_julia/OpenInterfacesImpl"}

[compat]
MsgPack = "1.2.1"
Optim = "2.0.1"
OrdinaryDiffEq = "6.102.1"
SciMLBase = "2.120.0"
22 changes: 14 additions & 8 deletions lang_julia/OpenInterfaces/src/OpenInterfaces.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export OIF_TYPE_INT,
OIF_TYPE_STRING,
OIF_TYPE_CALLBACK,
OIF_USER_DATA,
OIF_TYPE_USER_DATA,
OIF_TYPE_CONFIG_DICT

# Language ids
Expand Down Expand Up @@ -100,21 +101,26 @@ mutable struct OIFArrayF64
flags::Int32

function OIFArrayF64(arr::AbstractArray{Float64})
nd = ndims(arr)
dims = collect(Cssize_t, size(arr))
flags = OIF_ARRAY_F_CONTIGUOUS

if length(dims) == 1
flags = OIF_ARRAY_C_CONTIGUOUS | OIF_ARRAY_F_CONTIGUOUS
end

self = new(
ndims(arr),
Base.unsafe_convert(Ptr{Cssize_t}, dims),
Base.unsafe_convert(Ptr{Float64}, arr),
flags,
)
dimensions = Ptr{Cssize_t}(Libc.malloc(nd * sizeof(Cssize_t)))
unsafe_copyto!(dimensions, pointer(dims), nd)

self = new(nd, dimensions, Base.unsafe_convert(Ptr{Float64}, arr), flags)

finalizer(finalizing, self)

return GC.@preserve dims self
return self
end

function finalizing(self)
Libc.free(self.dimensions)
end
end

Expand Down Expand Up @@ -558,7 +564,7 @@ end
# otherwise, the will be crashes or something like that
# as Julia's garbage collector is pretty fast to remove reference
# that it thinks are unused.
function make_oif_user_data(data::Ref{Any})::OIFUserData
function make_oif_user_data(data)::OIFUserData
data_ptr = Base.unsafe_convert(Ptr{Cvoid}, data)
OIFUserData(OIF_LANG_JULIA, C_NULL, data_ptr, C_NULL)
end
Expand Down
4 changes: 2 additions & 2 deletions lang_julia/OpenInterfacesImpl/Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "OpenInterfacesImpl"
uuid = "daff4927-7858-4806-96da-915047fb441c"
authors = ["Dmitry Kabanov <dmitry.kabanov@uni-muenster.de>", "Stephan Rave <stephan.rave@uni-muenster.de", "Mario Ohlberger <mario.ohlberger@uni-muenster.de"]
version = "0.6.3"
authors = ["Dmitry Kabanov <dmitry.kabanov@uni-muenster.de>", "Stephan Rave <stephan.rave@uni-muenster.de", "Mario Ohlberger <mario.ohlberger@uni-muenster.de"]

[deps]
MsgPack = "99f44e22-a591-53d1-9472-aa23ef4bd671"
Expand All @@ -11,7 +11,7 @@ OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
SciMLBase = "0bca4576-84f4-4d90-8ffe-ffa030f20462"

[sources]
OpenInterfaces = {path = "../../lang_julia/OpenInterfaces"}
OpenInterfaces = {path = "../OpenInterfaces"}

[compat]
MsgPack = "1.2.1"
Expand Down
92 changes: 76 additions & 16 deletions lang_julia/OpenInterfacesImpl/src/callback.jl
Original file line number Diff line number Diff line change
@@ -1,29 +1,89 @@
module CallbackWrapper
export make_wrapper_over_c_callback

import SciMLBase
using OpenInterfaces

using OpenInterfaces: OIFArrayF64, OIF_ARRAY_C_CONTIGUOUS, OIF_ARRAY_F_CONTIGUOUS
function make_wrapper_over_c_callback(fn_c::Ptr{Cvoid}, oif_argtypes, oif_restype)::Function
c_argtypes = Tuple(
map(argtype -> begin
if argtype == OIF_TYPE_I32
Cint
elseif argtype == OIF_TYPE_F64
Cdouble
elseif argtype == OIF_TYPE_ARRAY_F64
Ref{OIFArrayF64}
elseif argtype == OIF_TYPE_USER_DATA
Ptr{Cvoid}
else
error("Unsupported argument type: $argtype")
end
end, oif_argtypes),
)

function make_wrapper_over_c_callback(fn_c::Ptr{Cvoid})::Function
function wrapper(t::Float64, y::Vector{Float64}, ydot::Vector{Float64}, user_data)::Int
if typeof(user_data) == SciMLBase.NullParameters
user_data = C_NULL
end
# Build a concrete Tuple type e.g. Tuple{Cdouble,Cdouble}.
# This IS a valid type parameter, unlike a plain tuple value (Cdouble, Cdouble).
c_argtypes_type = Core.apply_type(Tuple, c_argtypes...)

oif_y = Ref(OIFArrayF64(y))
oif_ydot = Ref(OIFArrayF64(ydot))
c_restype = if oif_restype == OIF_TYPE_I32 || oif_restype == OIF_TYPE_INT
# The second condition is just for explicitness, as `INT` is an alias to `I32`.
Cint
elseif oif_restype == OIF_TYPE_F64
Cdouble
else
error(
"Unsupported return type: $oif_restype. Only supported types are Cint and Cdouble",
)
end

@ccall $fn_c(
t::Float64,
oif_y::Ptr{OIFArrayF64},
oif_ydot::Ptr{OIFArrayF64},
user_data::Ptr{Cvoid},
)::Cint
function wrapper(args...)
c_args_from_jl_args = Tuple(
map(
(argtype, argvalue) -> begin
if argtype == OIF_TYPE_I32
argvalue
elseif argtype == OIF_TYPE_F64
argvalue
elseif argtype == OIF_TYPE_ARRAY_F64
OIFArrayF64(argvalue)
elseif argtype == OIF_TYPE_USER_DATA
argvalue === nothing ? C_NULL : argvalue
else
error("Unsupported argument type: $argtype")
end
end,
oif_argtypes,
args,
),
)

return 0
return GC.@preserve args c_args_from_jl_args call_c(
fn_c,
c_argtypes_type,
c_restype,
c_args_from_jl_args,
)
end

return wrapper
end

@generated function call_c(
fn_p,
::Type{argtypes},
::Type{restype},
args,
) where {argtypes,restype}

n = length(argtypes.parameters)

argtypes_expr = Expr(:tuple, map(t -> :($t), argtypes.parameters)...)
# Generate :(args[1]), :(args[2]), … at code-generation time so that ccall
# sees a fixed, statically-known argument list (runtime splatting is not allowed).
args_expr = [:(args[$i]) for i = 1:n]

quote
ccall(fn_p, $restype, $argtypes_expr, $(args_expr...))
end
end

end
5 changes: 5 additions & 0 deletions lang_julia/_impl/ivp/jl_diffeq/jl_diffeq.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export Self,
set_initial_value, set_rhs_fn, set_tolerances, integrate, set_user_data, set_integrator

using OrdinaryDiffEq
import SciMLBase

mutable struct Self
t0::Float64
Expand Down Expand Up @@ -79,6 +80,10 @@ function _rhs_wrapper(rhs)
# Note that the `p` argument will always be in the function signature
# regardless of if the problem is defined with parameters!
function wrapper(du, u, p, t)
if typeof(p) == SciMLBase.NullParameters
return rhs(t, u, du, nothing)
end

return rhs(t, u, du, p)
end
return wrapper
Expand Down
1 change: 1 addition & 0 deletions lang_julia/_impl/optim/optim_jl/optim_jl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ end

function set_objective_fn(self::Self, objective_fn)
self.objective_fn = objective_fn
self.objective_fn(self.x0, self.user_data)
end

function set_method(self, method_name, method_params)
Expand Down
83 changes: 81 additions & 2 deletions lang_julia/bridge_julia.c
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,85 @@ build_julia_tuple_from_size_t_array(intptr_t *dimensions, size_t ndims)
return tuple;
}

/**
* Build a Julia tuple from an `int32_t *` array.
*
* @param vals Array with elements of type int (32-bit integers)
* @param n Number of the elements in the array
* @return Julia tuple on success, `NULL` otherwise
*/
jl_value_t *
build_julia_tuple_from_oifargtype_array(OIFArgType *vals, size_t n)
{
char *tuple_string = malloc(sizeof(char) * 1024);
char *cursor = tuple_string;
char *right_bound = &tuple_string[1024 - 1];
intptr_t diff = right_bound - cursor;
snprintf(tuple_string, diff, "%s", "(");
cursor++;
char buffer[BUFFER_SIZE_];
int chars_written;
jl_value_t *tuple = NULL;

for (size_t i = 0; i < n; ++i) {
chars_written = snprintf(buffer, BUFFER_SIZE_, "%d", vals[i]);
assert(chars_written >= 1);
if (chars_written >= BUFFER_SIZE_) {
goto report_too_long;
}
diff = right_bound - cursor;
chars_written = snprintf(cursor, diff, "%s", buffer);
assert(chars_written >= 1);
if (chars_written >= diff) {
goto report_too_long;
}
cursor += chars_written;
diff = right_bound - cursor;
if (i < n - 1 || n == 1) {
chars_written = snprintf(cursor, diff, "%s", ",");
assert(chars_written >= 1);
if (chars_written >= diff) {
goto report_too_long;
}
cursor++;
}
}
diff = right_bound - cursor;
chars_written = snprintf(cursor, diff, "%s", ")");
if (chars_written >= diff) {
fprintf(stderr, "ERROR: the string to copy is too long\n");
exit(1);
}

tuple = jl_eval_string(tuple_string);
if (jl_exception_occurred()) {
const char *p = jl_string_ptr(
jl_eval_string("sprint(showerror, ccall(:jl_exception_occurred, Any, ()))"));
fprintf(stderr, "%s\n", p);
}
goto cleanup;

report_too_long:
fprintf(stderr,
"[build_julia_tuple_from_int32_t_array] The string representation of the julia "
"tuple does not fit in the buffer\n");

cleanup:
free(tuple_string);

return tuple;
}

static jl_value_t *
make_wrapper_over_c_callback(OIFCallback *p)
{
jl_value_t *wrapper = NULL;

jl_value_t *fn_p_c_wrapped = NULL;
jl_value_t *oif_argtypes = NULL;
jl_value_t *oif_restype = NULL;
JL_GC_PUSH3(&fn_p_c_wrapped, &oif_argtypes, &oif_restype); // NOLINT

if (CALLBACK_MODULE_ == NULL) {
jl_eval_string("using OpenInterfacesImpl.CallbackWrapper");
if (jl_exception_occurred()) {
Expand All @@ -224,10 +299,14 @@ make_wrapper_over_c_callback(OIFCallback *p)
jl_get_function(CALLBACK_MODULE_, "make_wrapper_over_c_callback");
assert(fn_callback != NULL);
assert(p->fn_p_c != NULL);
jl_value_t *fn_p_c_wrapped = jl_box_voidpointer(p->fn_p_c);
wrapper = jl_call1(fn_callback, fn_p_c_wrapped);
fn_p_c_wrapped = jl_box_voidpointer(p->fn_p_c);
oif_argtypes = build_julia_tuple_from_oifargtype_array(p->arg_types, p->nargs);
oif_restype = jl_box_int32(p->restype);

wrapper = jl_call3(fn_callback, fn_p_c_wrapped, oif_argtypes, oif_restype);

cleanup:
JL_GC_POP();
return wrapper;
}

Expand Down
14 changes: 13 additions & 1 deletion lang_python/oif/openinterfaces/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ class OIFCallback(ctypes.Structure):
("fn_p_c", ctypes.c_void_p),
("fn_p_jl", ctypes.c_void_p),
("fn_p_py", ctypes.c_void_p),
("nargs", ctypes.c_uint32),
("arg_types", ctypes.POINTER(OIFArgType)),
("restype", OIFArgType),
("hidden", ctypes.c_void_p),
]


Expand Down Expand Up @@ -197,7 +201,15 @@ def make_oif_callback(

fn_p_c = ctypes.cast(c_wrapper_fn, ctypes.c_void_p)
assert fn_p_c.value is not None
oifcallback = OIFCallback(OIF_LANG_PYTHON, fn_p_c, None, fn_p_py)

nargs = len(argtypes)
oif_arg_types = ctypes.cast(
(ctypes.c_int * nargs)(*argtypes), ctypes.POINTER(OIFArgType)
)

oifcallback = OIFCallback(
OIF_LANG_PYTHON, fn_p_c, None, fn_p_py, nargs, oif_arg_types, restype, None
)
return oifcallback


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def minimize(self, out_x):
)

out_x[:] = result.x
print(result)
return (result.status, result.message)

# def print_stats(self):
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ endif()
include(GoogleTest)

add_subdirectory(lang_c)
add_subdirectory(lang_julia)
2 changes: 2 additions & 0 deletions tests/lang_julia/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
add_library(oif_aux_funcs SHARED aux_funcs.c)
target_include_directories(oif_aux_funcs PRIVATE ${CMAKE_SOURCE_DIR}/include)
Loading
Loading