Using `symbol` in `ccall`

Dear all

Suppose that I have this Fortran module in file named MyModule.f90:

MODULE MyModule
  IMPLICIT NONE
  !
  REAL(8) :: x
  INTEGER :: Nsize
  REAL(8), ALLOCATABLE :: v(:)
END MODULE 

!-------------------------------
SUBROUTINE initialize_mymodule()
!-------------------------------
  USE MyModule
  IMPLICIT NONE
  integer :: i
  x = 1.23d0
  Nsize = 5
  if(.not. allocated(v)) then
    ALLOCATE( v(Nsize) )
    do i = 1,Nsize
      v(i) = 0.1d0 + i
    enddo 
  endif
  WRITE(*,*) 'MyModule is initialized'
END SUBROUTINE 

!-----------------------------
SUBROUTINE finalize_mymodule()
!-----------------------------
  USE MyModule
  IMPLICIT NONE
  !
  DEALLOCATE( v )
  WRITE(*,*) 'MyModule is finalized'
END SUBROUTINE

Build dynlib with gfortran:

gfortran -fPIC -shared MyModule.f90 -o libmymodule.so

Call from Julia:

const MYLIB = "./libmymodule.so"
# XXX: must specifiy as path string

function load_allocatable_array(symbol::Symbol, T::DataType, shape)
    @assert T == Float64
    ptr = cglobal( (symbol, MYLIB), Ptr{T} )
    tmp = zeros(T, prod(shape))
    for ip in 1:prod(shape)
        tmp[ip] = unsafe_load(unsafe_load(ptr,1),ip)
    end
    return reshape(tmp, shape)
end

function get_x()
    return unsafe_load(cglobal( (:__mymodule_MOD_x, MYLIB), Float64 ))
end

function get_x_v2()
    symb = :__mymodule_MOD_x
    return unsafe_load(cglobal( (symb, MYLIB), Float64 ))
end

function get_x_v3()
    symb = :__mymodule_MOD_x
    ptr = cglobal((symb, MYLIB), Float64)
    return ptr
end

function get_Nsize()
    return unsafe_load(cglobal( (:__mymodule_MOD_nsize, MYLIB), Int32 )) |> Int64 
end

# Should be called only once
ccall( (:initialize_mymodule_, MYLIB), Cvoid, () )

x = get_x()
#x = get_x_v2() # will crash
#x = unsafe_load(get_x_v3()) # will also crash
println("x = ", x)

Nsize = get_Nsize()
symb = :__mymodule_MOD_v
v = load_allocatable_array(symb, Float64, (Nsize,))
println("v = ", v)

Why get_x_v2() crash? Any difference with get_x(). I thought that both are the same (?).

This is tested on WSL2 and Julia 1.12.1:

julia> versioninfo()
Julia Version 1.12.1
Commit ba1e628ee49 (2025-10-17 13:02 UTC)
Build Info:
  Official https://julialang.org release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 16 × AMD Ryzen 7 5800H with Radeon Graphics
  WORD_SIZE: 64
  LLVM: libLLVM-18.1.7 (ORCJIT, znver3)
  GC: Built with stock GC
Threads: 1 default, 1 interactive, 1 GC (on 16 virtual cores)
Environment:
  JULIA_REVISE_POLL = 1

This is a reasonable assumption and would be correct if all involved functions were normal functions. Unfortunately cglobal is bit magical and requires literal arguments. This is not super easy to find out but the docstring says

cglobal((symbol, library) [, type=Cvoid])

Obtain a pointer to a global variable in a C-exported shared library, specified exactly as in ccall. Returns a Ptr{Type}, defaulting to Ptr{Cvoid} if no Type argument is supplied. The values can be read or written by unsafe_load or unsafe_store!, respectively.

and the note in the ccall documentation says that they must be literal values, and points to an eval trick, which can sometimes be useful to work around this limitation.

That said, there may also be bugs involved. Specifically I tried to check what LLVM code was generated but

@code_llvm get_x_v2()

gives a segmentation fault on Julia 1.12 but not on 1.10.

Edit: Crash reported in code_llvm crashes on non-literal in global · Issue #61019 · JuliaLang/julia · GitHub.

1 Like