How to embed multithreaded Julia in a graphical macOS app

I wrote a C++ plugin for the statistical software Stata to give it access to Julia. Stata is a GUI program, so in macOS, it is normally not launched from a shell. The problem I have is giving the user the option to launch it with nthreads > 1.

This is hard because on the Mac, I believe that graphical programs normally have no access to environment variables. So setting JULIA_NUM_THREADS is normally useless.

And the jl_init() function that launches an embedded copy of Julia takes no arguments. So I see no way to pass the equivalent of an “-t 8” command line option at launch.

Opening a terminal window and typing open -a StataMP does work, for then the graphical program is launched in a context with environment variables defined by .zshenv. But I would rather the user didn’t have to learn such tricks.

Is there a way to pass the equivalent of command line arguments to Julia when embedding it?

1 Like

I think you want to modify jl_options as defined in this header.

jl_options is a global declared in julia.h:

Oh interesting, thank you. But I’m still struggling.

I’m loading Julia dynamically, at runtime.
Simplifying somewhat, I first load libjulia with a command like

hDLL = dlopen(fulllibpath, RTLD_LAZY);

Then I get a pointer to jl_options:

JL_options = (jl_options_t *)dlsym(hDLL, "jl_options");

Then I set nthreads to 8:

JL_options->nthreads = 8;

And then I call jl_init()…and I get a seg fault.

So I’m not sure what to do, or what I’m doing wrong. It doesn’t crash if I skip the nthreads assignment.

I don’t think you need to use dlsym. If you include julia.h and then dlopen libjulia, then you should be able to access the symbol.

The other option is to use jl_parse_opts.

I don’t think you need to use dlsym . If you include julia.h and then dlopen libjulia, then you should be able to access the symbol.

OK, I tried that and got a compiler error:

unresolved external symbol __imp_jl_options

Perhaps that is related to the fact that I’m not linking to libjulia till runtime.

As for jl_parse_opts, I tried the following. It didn’t crash but also didn’t affect nthreads:

            int ac = 1;
            char* av = (char*)"--threads 8";
            char** avp = &av;
            JL_parse_opts(&ac, &avp);

I tried calling JL_parse_opts (my alias for the dynamically loaded jl_parse_opts()) both before and after JL_init(). Neither way did anything…

I see what you are doing.

Maybe call jl_init_options?

Looking at how pyjulia and PythonCall do it, they both go through jl_parse_opts.

You might also need to set nthreadpools if you are going to set it directly.

Looking at those examples is a good idea. You’re right that’s how they do it. Still haven’t figured it out how to make it work for me.

Here’s a demonstration that seems to work for me on nightly:

#include "julia.h"

int main() {
    // configure jl_options
    jl_options.nthreadpools = 1;
    jl_options.nthreads = 8;
    int16_t ntpp[] = {jl_options.nthreads};
    jl_options.nthreads_per_pool = ntpp;

    // initialize Julia
    jl_init();
    jl_eval_string("println(\"Number of threads:\")");
    jl_eval_string("println(Threads.nthreads())");
    jl_atexit_hook(0);

    return 0;
}

To compile I ran this:

./julia usr/share/julia/julia-config.jl  --allflags | xargs gcc embed_threads_demo.c -o embed_threads_demo

Here’s the output of julia-config.jl:

$ ./julia usr/share/julia/julia-config.jl  --allflags 
-std=gnu11 -I'/home/mkitti/src/julia/usr/include/julia' -fPIC -L'/home/mkitti/src/julia/usr/lib' -Wl,--export-dynamic -Wl,-rpath,'/home/mkitti/src/julia/usr/lib' -Wl,-rpath,'/home/mkitti/src/julia/usr/lib/julia' -ljulia

ldd reports the following:

$ ldd ./embed_threads_demo 
	linux-vdso.so.1 (0x00007ffe1a002000)
	libjulia.so.1.12 => /home/mkitti/src/julia/usr/lib/libjulia.so.1.12 (0x00007f4e5987e000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4e59600000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f4e598ae000)

I then get the following result:

$ ./embed_threads_demo 
Number of threads:
8

Let me see if I can figure out how to do this with jl_parse_opts.

3 Likes

Here’s the version via jl_parse_opts:

#include "julia.h"

int main(int org_argc, char* org_argv[]) {
    // parse options
    int argc = 2;
    char** argv = malloc(sizeof(char*)*argc);
    // first argument is the name of the executable
    argv[0] = org_argv[0];
    argv[1] = "--threads=6";
    jl_parse_opts(&argc, &argv);

    // initialize Julia
    jl_init();
    jl_eval_string("@info \"debug\" Threads.nthreads()");
    jl_atexit_hook(0);

    return 0;
}

It is compiled in the same way as above.

$ ./julia usr/share/julia/julia-config.jl  --allflags | xargs gcc embed_threads_demo_parse_args.c -o embed_threads_demo_parse_args

$ ./embed_threads_demo_parse_args 
┌ Info: debug
│   Threads.nthreads() = 6
└ @ Main none:1
2 Likes

Finally, here’s how to do this via dlopen.

#include <stdlib.h>
#include <dlfcn.h>

int main(int org_argc, char* org_argv[]) {
    // dynamically load libjulia
    const char* libjulia_path = "usr/lib/libjulia.so";
    void* jlptr = dlopen(libjulia_path, RTLD_LAZY);
    void (*jl_parse_opts)(int*, char***) = dlsym(jlptr, "jl_parse_opts");
    void (*jl_init)() = dlsym(jlptr, "jl_init");
    void* (*jl_eval_string)(const char*) = dlsym(jlptr, "jl_eval_string");
    void (*jl_atexit_hook)(int) = dlsym(jlptr, "jl_atexit_hook");

    // parse options
    int argc = 2;
    char** argv = malloc(sizeof(char*)*argc);
    // first argument is the name of the executable
    argv[0] = org_argv[0];
    argv[1] = "--threads=4";

    // parse arguments
    jl_parse_opts(&argc, &argv);

    // initialize Julia
    jl_init();
    jl_eval_string("@info \"debug\" Threads.nthreads()");
    jl_atexit_hook(0);

    return 0;
}

I compile and run as follows.

$ gcc embed_threads_demo_parse_args_dlopen.c -o embed_threads_demo_parse_args_dlopen -ldl

$ ./embed_threads_demo_parse_args_dlopen 
┌ Info: debug
│   Threads.nthreads() = 4
└ @ Main none:1
4 Likes

You probably should be using Jluna (since you’re calling from C++, it improves on the C API of Julia for C++, maybe even you should use this if your code were C code?):

I find the installation process for that overwhelmingly complicated, especially if I have to do it in three platforms. I’m developing for Windows, Linux, and macOS, and I didn’t know all of them well.

I wouldn’t know, I’ve not even used Jluna for one platform! I just wanted to help, if the C API directly (or some other wrapper helps) is enough for you then great! It seems you have good help already, reading the thread. It just seemed to me that Jluna is the go-to-solution for C++, though it may be overkill for you, or just for enabling multi-threading.

It seems sensible that GUI apps on macOS do not have access to ENV vars, and I believe that’s also done by now on Windows, at least for some types of GUI apps.

But in either case, it seems like ENV vars may work, in the sense that setting them from within your app, for further processing from it, for its benefit, seems still sensible, and I don’t see a reason why it shouldn’t work or be blocked.

It seems if I’m reading the right code, that’s what Jluna does:

so if it doesn’t work it will be surprising for users, and I and others would like to know! About Jluna being “overwhelmingly complicated” (to install), it’s at least meant to be a better C++ API, and the only one claiming that. Whatever you do, I would like to know how it goes, if you or other can embed into a larger (cross-platform) GUI app.

If you were just making a cross-platform (GUI) app, with Julia as the only language, or the main one, I would point you to AppBundler.jl. I’m not sure it helps you since you do not start with Julia (or how well it would fit with Jluna, probably not going to hurt, nor help much), and call e.g. C++/Stata (could you?). I’m not sure, maybe it can also help if Julia is being called? It’s a rather new project, I would like to know more about app building across languages (and operating systems), people claim difficult for Python alone, harder for Julia alone, and even more combining (on recent Reddit thread). Tools are likely not ready for that (by default), but might be improved…

I’m sure someone has embedded Julia before, for any of the platforms (should be supported on all), even with threading, not sure anyone has done for all three yet, or a GUI app. It seems like necessarily more complex, or at repeated compilation 3x. Do you think Jluna could do something to make it better? About changing code, or just adding docs?

1 Like

Thank you @mkitti. This is tremendously helpful. The key thing I learned is “first argument is the name of the executable”. I was putting “–threads=7” in argv[0].
In my case I am compiling to a shared library, not an executable, so technically the caller is Stata. But I found I can just set argv[0] to 0 in your example and it still works.

It is now possible for a Stata user to type jl start, threads(7) and launch Julia with 7 threads.

3 Likes