Memory management and PackageCompiler libraries

Hello,

I have a question regarding memory management when retrieving arbitrary sized memory blocks from a library compiled by the PackageCompiler.

In https://github.com/originalsouth/RandomPack I have created a dummy project where the file src/RandomPack.jl returns arbitrary sized Cstrings.

After running make -C build the C program with its source in build/RandomPack.c retrieves a bunch of Cstrings from the compiled Julia implementation and prints them – which works :smiley:

My questions, however, what is the lifetime of the memory allocated for the strings generated in const char *random_pack(const char *str), what part of the code ought to be responsible for the memory management, and how can I control (not) freeing the memory up?

Things I have played with:

  • Calling void free(void *ptr) from the C-code which crashes the Julia library
  • Calling GC.gc() after every Julia side function call (this makes the code slow and invalidates the pointers indeed)

Thanks a lot in advance,
Cheers!

You can “root” the object by storing it in a global array for example. Then the Julia GC will not clean it up.

1 Like

I would recommend you copy the string. So in your Julia code, you manually allocate a unmanaged memory and copy the content to this memory area, then you free this memory in your C code after usage. The pseudo code looks like this:

Base.@ccallable function random_pack(input::Cstring)::Cstring
    jinput = unsafe_string(input)
    str = randstring(rand(length(jinput):rand(2:12)*length(jinput)))
    finalstr =  jinput * ":" * str
    ptr = Base.Libc.malloc(sizeof(finalstr))
    # maybe gc preserve is needed here, we copy the content to ptr
    memcpy(ptr, finalstr)
    return Base.Cstring(ptr)
end

Thanks! A global Set as “root” with a release call, feels wrong but I guess that is the way to go :slight_smile:

Edit: There seems to a similar solution here…

I like this idea, especially when working with small objects.

I tried implementing it but it somehow still leaks when valgrinding…

Base.@ccallable function random_pack(input::Cstring)::Cstring
    jinput = unsafe_string(input)
    str = randstring(rand(length(jinput):rand(2:12)*length(jinput)))
    retval = jinput * ":" * str
    ptr = Base.convert(Ptr{UInt8}, Base.Libc.malloc(1 + length(retval)))
    Base.unsafe_copyto!(ptr, pointer(Base.cconvert(Cstring, retval)), 1 + length(retval))
    return Base.Cstring(ptr)
end

Probably, I am still doing something wrong.

Sorry for the late reply. I think you need to preserve retval getting gc by using Base.GC.@preserve. Here is the complete code.

function string_copy(s::String)
    # we copy codeunit here
    nc = ncodeunits(s)
    # 1 for '\0', I am not sure whether Julia's string is zero terminated, so I just add my own one
    ptr = Base.unsafe_convert(Ptr{UInt8}, Base.Libc.malloc(nc + 1))
    Base.unsafe_store!(ptr, UInt8('\0'), nc + 1)
    # we mark the string as in use, to prevent pointer getting gc
    Base.GC.@preserve s begin
        str_ptr = Base.unsafe_convert(Ptr{UInt8}, s)
        Base.unsafe_copyto!(ptr, str_ptr, nc)
    end
    return Base.Cstring(ptr)
end

Test code:

let
    x = "1,2,3"
    c_str = string_copy(x)
    Base.unsafe_string(c_str) == x
    Base.Libc.free(c_str)
end

Thank you for you reply!

Ah, right the terminating zero might have caused the missed reads, in some cases – let me test.

Yet, I am a little confused still with the preserve.
I think the memory allocated by malloc is outside of the GC and thus needs to be freed whenever we are done. The pointer itself (i.e. the memory address of the allocated memory), however, can and should be deleted by the GC once we have handed it over to the C-side. Are we therefore not causing a memory leak preserving the pointer?

We have two pointers here: the pointer allocated by malloc and the underlying pointer of String. The latter is managed by GC. We need to ensure this pointer is valid (not gc by Julia) when we copy the content of this pointer to the manually allocated one. That’s why we need to preserve the string (we do not preserve the manually allocated pointer). Also, GC.@preserve only take effects locally. For example:

Base.GC.@preserve s begin
  str_ptr = Base.unsafe_convert(Ptr{UInt8}, s)
  Base.unsafe_copyto!(ptr, str_ptr, nc)
end

After the evaluation of begin block, s is not preserved (s is only preserved during the evaluation of the block`).

1 Like

Thanks! It all makes sense now :smiley: