Segfault with ccall when the code is loaded as package

Hi, I found some strange behavior which I am not sure is intended. Maybe I am missing something totally obvious, in which case I apologize. I found the following MWE.

First, create a new package

~$ julia
(@v1.6) pkg> generate TestCCall
  Generating  project TestCCall:
    TestCCall/Project.toml
    TestCCall/src/TestCCall.jl

and set the content of TestCCall/src/TestCCall.jl to this

module TestCCall

f(::Ptr{UInt8})::Cvoid = nothing
c_f = @cfunction(f, Cvoid, (Ptr{UInt8},))
const const_c_f = @cfunction(f, Cvoid, (Ptr{UInt8},))

function a()
    @show c_f
    ccall(c_f, Cvoid, (Ptr{UInt8},), C_NULL)
end

function b()
    @show const_c_f
    ccall(const_c_f, Cvoid, (Ptr{UInt8},), C_NULL)
end

end # module

When I load the package and call a(), it throws UndefRefError, whereas b() results in Segmentation fault. I expected that both calls would work the same and without any error. See:

~$ cd TestCCall/
~/TestCCall$ julia --project
julia> using TestCCall

julia> TestCCall.a()
c_f = Ptr{Nothing} @0x0000000000000000
ERROR: UndefRefError: access to undefined reference
Stacktrace:
 [1] a()
   @ TestCCall ~/TestCCall/src/TestCCall.jl:11
 [2] top-level scope
   @ REPL[2]:1

julia> TestCCall.b()
const_c_f = Ptr{Nothing} @0x00007fef0690cae0

signal (11): Segmentation fault
in expression starting at REPL[3]:1
unknown function (ip: 0x7fef0690cae0)
b at /home/user/TestCCall/src/TestCCall.jl:16
unknown function (ip: 0x7f1504c531bc)
unknown function (ip: 0x7f154c144ab5)
unknown function (ip: 0x7f154c1450bb)
unknown function (ip: 0x7f154c145d70)
unknown function (ip: 0x7f154c1610f7)
unknown function (ip: 0x7f154c161d9c)
jl_toplevel_eval_in at /usr/bin/../lib/julia/libjulia-internal.so.1 (unknown line)
unknown function (ip: 0x7f153a02ed43)
unknown function (ip: 0x7f153a02f314)
unknown function (ip: 0x7f153a02f44d)
unknown function (ip: 0x7f153a05c87f)
unknown function (ip: 0x7f153a05c938)
unknown function (ip: 0x7f153a242aa8)
unknown function (ip: 0x7f153a242b48)
jl_f__call_latest at /usr/bin/../lib/julia/libjulia-internal.so.1 (unknown line)
unknown function (ip: 0x7f153a120d8b)
unknown function (ip: 0x7f153a12f8d3)
unknown function (ip: 0x7f153a130d2c)
unknown function (ip: 0x7f153a130e95)
unknown function (ip: 0x7f154c18433e)
repl_entrypoint at /usr/bin/../lib/julia/libjulia-internal.so.1 (unknown line)
main at julia (unknown line)
__libc_start_main at /usr/bin/../lib/libc.so.6 (unknown line)
_start at julia (unknown line)
Allocations: 2649 (Pool: 2639; Big: 10); GC: 0
Segmentation fault (core dumped)

However, if I directly include the source file (which is not my intention) without “loading the package”, it works without errors:

~/TestCCall$ cd src/
~/TestCCall/src$ julia
julia> include("TestCCall.jl")
Main.TestCCall

julia> TestCCall.a()
c_f = Ptr{Nothing} @0x00007f62286af940

julia> TestCCall.b()
const_c_f = Ptr{Nothing} @0x00007f62286afa40

My Julia version:

Julia Version 1.6.1
Commit 6aaedecc44* (2021-04-23 05:59 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Core(TM) i5-4258U CPU @ 2.40GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-12.0.0 (ORCJIT, haswell)

Am I doing something wrong, or is it a bug?

From the manual:

Constants involving most Julia objects that are not produced by ccall do not need to be placed in __init__ : their definitions can be precompiled and loaded from the cached module image. This includes complicated heap-allocated objects like arrays. However, any routine that returns a raw pointer value must be called at runtime for precompilation to work ( Ptr objects will turn into null pointers unless they are hidden inside an isbits object). This includes the return values of the Julia functions @cfunction and pointer .

https://docs.julialang.org/en/v1/manual/modules/

Thanks!

It means that it is impossible to define const const_c_f = @cfunction(f, Cvoid, (Ptr{UInt8},)). Instead, the __init__ function has to set the @cfunction; either using a global variable or a constant Ref.

Is that correct?

Yes.
I’d reccomend using const C_F_REF = Ref(C_NULL), and setting that in your __init__().

The advantage of setting it to C_NULL is so that if __init__ somehow fails and it doesn’t get updated, that’ll result in an UndefRefError instead of a segfault. Note that c_f === C_NULL but that const_c_f had some non-null value.

Oh, ok, got it. Thanks for your explanation!