Changing array values in place when embedding in C++

Hello,

I may be missing the obvious here, hopefully someone can point it out to me: when calling a custom julia function from C++ passing an array of doubles, the array values should be changed in place but is not.

For example, the Julia function definition

Base.@ccallable function square_array(x::Ptr{Cdouble}, N::Cintmax_t)::Cint
    x = array_wrap(x, (N))
    x .*= 2
    0
end

The corresponding call in C++

#include <iostream>
#include <julia.h>

JULIA_DEFINE_FAST_TLS()

int main()
{
    jl_init_with_image__threading(NULL, "/path/to/image.so");
    {
        jl_function_t* func  = jl_get_function(jl_main_module, "square_array");
        if(func) {
            jl_value_t** args;
            const int dataN = 2;
            const int nargs = 2;
            JL_GC_PUSHARGS(args, nargs);

            jl_value_t* double_type_1d = jl_apply_array_type((jl_value_t*)jl_float64_type, 1);
            jl_array_t* x = jl_alloc_array_1d(double_type_1d, dataN);

            double* xData = (double*)jl_array_data(x);
            xData[0] = 1.3;
            xData[1] = 4.6;

            for(int i = 0; i < dataN; ++i) {
                std::cout << i << ": " << xData[i] << std::endl;
            }

            args[0] = (jl_value_t*)x;
            args[1] = jl_box_int64(dataN);

            std::cout << "starting test..." << std::endl;
            jl_value_t* val = jl_call(func, args, nargs);
            JL_GC_PUSH1(&val);
            
            std::cout << "test finished: " << val << std::endl;

            for(int i = 0; i < dataN; ++i) {
                std::cout << i << ": " << xData[i] << std::endl;
            }

            // cleanup
            JL_GC_POP();

        } else {
            std::cout << "Function square_array() not found!" << std::endl;
        }
    }
    jl_atexit_hook(0);
}

However looking at a similar example in C (from C/eval_julia_personal_function_array_args/embed_example.c · master · Jamil_Projects / Embedded_Julia / Julia_embed · GitLab), the following will make the change in place.

#include <julia.h>

//JULIA_DEFINE_FAST_TLS() // only define this once, in an executable (not in a shared library) if you want fast code.

#include <stdio.h>

int main(int argc, char *argv[])
{
 
    printf("THIS CODE IS TO SQUARE ALL ELEMENTS IN AN ARRAY AND RETURN THE NEW ARRAY IN A JULIA MODULE");
    /* required: setup the Julia context */
    jl_init();

    /* create a 1D array of length 100 */

    double length = 5;
    double *existingArray = (double*)malloc(sizeof(double)*length);

    /* create a *thin wrapper* around our C array */
    jl_value_t* array_type = jl_apply_array_type((jl_value_t*)jl_float64_type, 1);
    jl_array_t *x = jl_ptr_to_array_1d(array_type, existingArray, length, 0);
    JL_GC_PUSH1(&x);


    
    /* fill in values */
    double *xData = (double*)jl_array_data(x);

    for(size_t i=0; i<jl_array_len(x); i++)
        xData[i] = i;
    printf("x = [");
    for(size_t i=0; i<jl_array_len(x); i++)
            printf("%i ", (int)xData[i]);
    printf("]\n");

    /* import `jamil_math` module from file jamil_math.jl */
    jl_eval_string("Base.include(Main, \"jamil_math.jl\")");

    /* load the module into the current stack*/
    jl_eval_string("using .jamil_math");

    /* convert the module into a C module*/
    jl_module_t* jamil_math_module = (jl_module_t *)jl_eval_string("module_math");

    /* get `square_array` function from module */
    jl_function_t *square_array = jl_get_function(jamil_math_module, "square_array");

    /* call the function */
    jl_call1(square_array, (jl_value_t*)x);

    /* print new values of x */
    printf("new values after being passed into julia function");
    printf("\nx = [");
    for (int i = 0; i < length; i++)
        printf("%.1f ", xData[i]);
    printf("]\n");
    

    JL_GC_POP();

    /* exit */
    jl_atexit_hook(0);
    
    printf("\n\n\n");

    return 0;

}

If I call the function from Python (similarly loading dynamically the .so/.dll), the values are changed in place as expected. Here it is in Python

from ctypes import c_long, c_void_p
import numpy as np

julia_lib = "/path/to/julia/lib.so"
custom_image = "/path/to/custom/lib.so"


internal_jl = ctypes.CDLL(julia_lib)
api = ctypes.CDLL(custom_image, ctypes.RTLD_GLOBAL)

internal_jl.jl_init_with_image__threading.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
internal_jl.jl_init_with_image__threading(None, bytes(custom_image, 'utf-8'))

x = 2 * np.ones(3)
api.square_array(c_void_p(x.ctypes.data), c_long(3))
print(x)
# => prints [4., 4., 4.]

So I can’t get my head around why the first c++ snippet code doesn’t change the value in place although it should, any idea?
thanks!

Toying around a bit with your code it seems square_array is never actually called. Checking for an exception directly after the jl_call() reveals a MethodError is raised. This is likely due to the fact that the x argument being passed is an Array{Float64,1} and not a Ptr{Cdouble} (although I don’t know the details of whether that can be made to work when embedding).

Updating the signature of square_array to use x::Vector{Float64} makes the function get called, but array_wrap is then unknown, but is also no longer needed as you can modify the array directly and it will get updated on the C++ side as well.

hmmm, interesting, I completely missed that part: if you don’t mind, how did you manage to get the error message sent back to you (I couldn’t get anything with try/catch statements)?

So it looks like I need 2 functions to accommodate both cases, one for the call from Python through C (which passes void*), one for the call from C/C++ (which passes directly Vector/Matrix{Float64}).

(the array_wrap() is needed when the ccallable function gets a void* so that it can reshape to its proper/actual size - and apparently is not necessary for the C/C++ bridge to Julia through jl_call())

I’ll give it a try and report back
thanks for the insight

1 Like

You can check for exceptions on the C/C++ side with for example

if (jl_exception_occurred())
    printf("%s \n", jl_typeof_str(jl_exception_occurred()));

See this section for more details

Thanks @paulmelis I confirm that you got it right: the function signature in Julia does not need to be ccallable, the Julia directly recognizes the Vector/Matrix(Float64}

In my case, I simply need 2 functions with different signature to call the same core code so that it can handle calls from both C++ and Python. Something like

Base.@ccallable function c_double_array(x::Ptr{Cdouble}, N::Cintmax_t)::Cint
    x = array_wrap(x, (N))
    double_array(x)
end

function double_array(x)
    x .*= 2
    0
end