AOT compiling using PackageCompiler


#1

Hello, I’m enchanted with the idea of bringing performant julia code into a python script through a shared library, especially if it as easy as PackageCompiler seems to imply. I am almost entirely unfamiliar with compiled languages, so much of this feels new to me.

I got a simple script to work, but now the problem I am having is with the following seemingly simple script:

test.jl

Base.@ccallable function func0()::Cdouble
    return sum([1.34, .7])
end

Base.@ccallable function func1()::Cdouble
    return (1.2 + .1) / 2.0
end

I compile the file to a shared library with julia juliac.jl -vas test.jl

Calling func1 from python works fine, but func0 causes a segfault. I have this same problem calling any imported function, and I don’t understand why. I imagine I am misunderstanding something fundamental.

Thank you in advance!


#2

I think @ccallable functions might need to use valid C, so both the square brackets and sum() could be trouble

From the compiling an executable section

Base.@ccallable function julia_main(ARGS::Vector{String})::Cint
 hello_main(ARGS)  # call your program's logic. ( using julia code )
 return 0
end

#3

I’m not sure what’s causing the segfault, but this is certainly not the case. There’s no restriction to the type of Julia code you can use (at least if you don’t explicitly disable the JIT compiler when you call julia_init in your driver program).

Have you tried build_executable with func0 as the julia_main function? That should definitely work, and the C program it produces should give you something to pattern match if you’re trying to build a shared library with multiple ccallable functions.


#4

Did you ever initialize the Julia runtime (IIRC via julia_init)? If not, it may be possible that func1 only runs because it doesn’t need to invoke the compiler, but func0 does, which will fail because Julia wasn’t initialized.


#5

I find correctly initializing it is a bit tricky. But it works for me:

from pathlib import Path
import ctypes

libpath = Path(__file__).resolve().parent.joinpath("builddir", "test.so")
dll = ctypes.CDLL(libpath, ctypes.RTLD_GLOBAL)
dll.func0.restype = ctypes.c_double
dll.func1.restype = ctypes.c_double

try:
    jl_init_with_image = dll.jl_init_with_image
except AttributeError:
    jl_init_with_image = dll.jl_init_with_image__threading
jl_init_with_image.argtypes = [ctypes.c_char_p, ctypes.c_char_p]


if __name__ == "__main__":
    jl_init_with_image(None, str(libpath).encode("utf-8"))
    print("func1 =", dll.func1())
    print("func0 =", dll.func0())

#6

this works, thank you! i will now have to understand exactly what you did!


#7

Are you able to post a complete example (Julia and Python code)?
I’m having a lot of trouble getting this to work on Linux.

E.g., the first error I get says that juliac.jl needs to Pkg.add("ArgParse"), which suggests we are starting from different states.


#8

Here is a (hopefully) reproducible example:

Check out the repository and run make.

But you guys should be using PyJulia :wink:


#9

Oops I forgot to add the Makefile (it’s added now).


#10

Note by the way that without a snoopfile (see build_executable call in the usage example), the functions are still compiled the first time they’re called from C or Python with given input types, as opposed to having their native code (for a finite set of input type tuples) incorporated into the system image.


#11

Thanks @tkf that’s most helpful. I have this working.

My next challenge is to mimic test.py in Julia (call the library functions from Julia).

The use case is a set of users who each request a batch computation of about 30 mins running time. Each user has some idiosyncratic code that lives in its own library, so I only have to load the library for the requesting user rather than load the code for all users.

Here’s what I have so far (posted here, apologies for the double post):

using Libdl
test = Libdl.dlopen("test.so")
func0 = Libdl.dlsym(test, :func0)  # Returns a Ptr(Nothing)
func0()  # segfault

I’m not initializing the runtime at all, and test doesn’t have a jl_init_with_image handle exposed. Any idea how to get this going?


#12

Ah ok.

For my own inner peace, does this mean that the compiler will compile all functions that are called from a given snoopfile (for the arg signature actually called), including those called indirectly as dependencies?


#13

The translation of the Python script (without jl_init_with_image) would be something like

test = Libdl.dlopen("test.so", RTLD_GLOBAL)
func0 = Libdl.dlsym(test, :func0)
ccall(func0, Cdouble, ())

but it segfaults too.

The shared library test.so links to libjulia and (I think) full Julia runtime is available. So why don’t you load the Julia code directly?


#14

The aim is to load a module conditional on the script input. E.g., if ARGS[1] == x load module A, else load module B.

Currently I have to load all modules. A shared library would enable conitional loading.


#15

Why not something like @eval using $(Symbol(ARGS[1]))?