Conditional code and precompilation

I am developing a package that is designed to be used either by standard Julia or by Julia library embedded in a third party application. Most of the code is the same in either mode, but a few functions are currently defined conditionally like this

const embedded = isembedded()  # true when embedded, false otherwise
if embedded
    f() = "one implementation"
else
    f() = "another implementation"
end

Unfortunately, this approach does not work if I want to have my package precompiled. Since pre-compilation is always done by a standard julia instance, the precompiled code will always have embedded set to false.

I could probably redefine Base.julia_cmd() and Base.LOAD_CACHE_PATH[1] in the embedded case, but this will result in a duplicate cache for all packages, not only mine.

What is the recommended approach in a situation like this? I thought this problem would be resolved in pyjulia, but it looks like it is not.

One solution that comes to mind is to move the variable code to the __init__ method:

function __init__()
    if isembedded()
        global f() = "one implementation"
    else
        global f() = "another implementation"
    end
end

Why not

function f()
    if isembedded()
         "one way"
    else
        "other way"
    end
end

Is the runtime cost of isembedded() really so bad?

Use @static if.

2 Likes

Can you elaborate on this? I don’t see how the @static macro will help. It will be expanded during precompilation and embedded system will still see the non-embedded code.

The problem is actually harder than I thought. The main source of differences between embedded and non-embedded codes is in the way I call the third-party API. In the embedded mode, the API function f can be called simply as

ccall(:f, ...)

because the symbol f is in the main binary. In the embedded mode, the API calls must include the path to the dynamic library:

const lib = "path/to/lib.so"
ccall((:f, lib), ...)

I currently deal with this by conditionally defining a macro that given :f would expand into either :f or (:f, lib). I borrowed this approach from PyCall.

Since ccall requires a compile-time constant I would have to replace every instance of

f() = ccall((@sym :f), ...)

with

function f()
   if isembedded()
       ccall(:f, ...)
   else
       ccall((:f, lib), ...)
   end
end

but I am afraid this will still attempt to dlopen the library while running in the embedded mode. This will not work because the application is statically linked and the API implementation in the app is not compatible with that in the library. I am not sure how pyjulia copes with this. Most likely in will not work properly in a statically linked python.

Don’t you want to make it compile to have one function when embedded and one function when not?

@static will be exactly the same as what you were doing so it won’t solve any problem here.

If isembedded() is expensive you can just cache it in __init__ to a global and branch on that.

but I am afraid this will still attempt to dlopen the library while running in the embedded mode.

That should not be a problem. The dlopen will fail but as long as your code doesn’t execute that branch no error would be raised.

1 Like

Let me make the problem more specific. My function f is a Julia wrapper to the namesake 3rd party C function. The 3rd party API comes in two flavors: it is available to plugins running inside an application or can be loaded from a DLL. In the first case, my function is simply

f() = ccall(:f, ...)

while in the second, the path to the library should be specified

f() = ccall((:f, lib), ...)

Thanks, @yuyichao - I’ll try that. What do you think about the following alternative:

const N = .. # Number of the API functions
const API = zeros(Ptr{Void}, N)
f() = ccall(API[1], ...)
...  #  all the other API functions

function __init__()
   if !isembedded()
      Libc.dlopen(...)
   end
   API[1] = cglobal(:f)
   ...  #  all the other API functions
end

This will still incur the indirection overhead for each API call, but the code size will be 2x smaller.

You should use dlsym instead of cglobal and you can use individual slots instead of an Array.
And you should be able to write a macro do that at the call side. I thought that’s what PyCall used at some point.

PyCall does the following:

if libpython == nothing
    macro pysym(func)
        esc(func)
    end
else
    macro pysym(func)
        :(($(esc(func)), libpython))
    end
end

and uses @pysym :f at the call sites. This approach does not work.

Yes, I know what PyCall is doing now and I know it doesn’t work.
I said I thought it did something else before.

e.g. https://github.com/JuliaPy/PyCall.jl/commit/600a7f5b6d3abb6c54e8849817bb003de6b5c965

1 Like