Julia: Calling C Module

In one specific task, I need to extract the DCT coefficients from a JPEG image and export them into Julia. Since there are a number of existing C libraries that are capable of doing this, it is more convenient to call them directly with Julia.

The library

In order to extract DCT coefficients from JPEG images, we need to call libjpeg APIs. Fortunately, there has been existing wrappers on GitHub that encapsulates the complicated libjpeg function calls. To download the package, we can clone the repository to the Julia source code’s directory.

git clone https://github.com/klauscc/jpeg_toolbox_python.git

Since we are not building the Python bindings, we can comment out the last line in CMakeLists.txt:

#add_subdirectory(python)

And then we build the dynamic library inside the build folder.

$ mkdir build
$ cd build
$ cmake ..
$ make

If everything is successful, we should be able to see libextract_dct.so in build directory.

$ ls
CMakeCache.txt	cmake_install.cmake  Makefile
CMakeFiles	libextract_dct.so    test_jpeg_read

Exposing C structures and functions to Julia

There are two functions provided by the C package, namely read_jpeg and freeJpegObj. We need to export these two functions to Julia so that we can use its results. Firstly, we define a Julia structure that is identical to jpegobj in C.

struct CJpegObj
    image_width::Int32
    image_height::Int32
    image_components::Int32
    image_color_space::Int32
    quant_nums::Int32
    coef_array_shape::NTuple{4, NTuple{2, Int32}}
    quant_tables::Ptr{Float64}
    coef_arrays::Ptr{Ptr{Float64}}
end

Then, we load the functions from the dynamic library into Julia.

using Libdl

jpegTool = Libdl.dlopen("jpeg_toolbox_python/build/libextract_dct.so")
read_jpeg = Libdl.dlsym(jpegTool, :read_jpeg)
free_jpeg_obj = Libdl.dlsym(jpegTool, :freeJpegObj)

We can create a Julia type to store the data from C library and use a function to copy data from C to Julia in order to properly manage resource.

mutable struct JpegObj
    image_width::Int32
    image_height::Int32
    image_components::Int32
    image_color_space::Int32
    quant_nums::Int32
    coef_array_shape::Array{Int32, 2}
    quant_tables::Array{Array{Float64, 2}, 1}
    coef_arrays::Array{Array{Float64, 2}, 1}
end

using Base.Filesystem


# this function can create potential memory leak when there is an exception!
function read_dct_coef(imgname::String)
    @assert isfile(imgname)

    # get object
    cJpegObj = ccall(read_jpeg, CJpegObj, (Cstring,), imgname)

    # copy the data from C to julia
    jpegObj = JpegObj(
        cJpegObj.image_width,
        cJpegObj.image_height,
        cJpegObj.image_components,
        cJpegObj.image_color_space,
        cJpegObj.quant_nums,
        Array{Int32, 2}(undef, 3, 2),
        Array{Array{Float64, 2}, 1}(undef, 0),
        Array{Array{Float64, 2}, 1}(undef, 3)
    )

    println(cJpegObj)

    # copy dct array shapes
    for i in 1:3, j in 1:2
       jpegObj.coef_array_shape[i, j] = cJpegObj.coef_array_shape[i][j]
    end

    # copy quantization tables
    # always assume dct size is 8
    @assert jpegObj.quant_nums >= 1
    jpegObj.quant_tables = fill(Array{Float64, 2}(undef, 8, 8), jpegObj.quant_nums)
    for i in 1:jpegObj.quant_nums, x in 1:8, y in 1:8
        offset = (i - 1) * 64 + (x - 1) * 8 + y
        jpegObj.quant_tables[i][x, y] = unsafe_load(cJpegObj.quant_tables, offset)
    end


    # copy dct arrays
    for i in 1:3
        rows = jpegObj.coef_array_shape[i, 1]
        cols = jpegObj.coef_array_shape[i, 2]
        jpegObj.coef_arrays[i] = Array{Float64, 2}(undef, rows, cols)
        dataPtr = unsafe_load(cJpegObj.coef_arrays, i)
        for x in 1:rows, y in 1:cols
            offset = (x-1) * cols + y
            jpegObj.coef_arrays[i][x, y] = unsafe_load(dataPtr, offset)
        end
    end

    # free object
    ccall(free_jpeg_obj,Ptr{Cvoid} ,(CJpegObj,), cJpegObj)

    jpegObj
end