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