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?
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.