Hi everyone! I'm trying to create GObject bindings...
# kotlin-native
r
Hi everyone! I'm trying to create GObject bindings in Kotlin/Native, and I need to instantiate a GObject Record, which is essentially a C struct. I’m aware of
memScoped
for scoped memory management, but I’m looking for something closer to Java's `Arena.ofAuto()`: a GC-managed memory arena where allocations are automatically cleaned up by the runtime without relying on scope-based disposal. Is anything like this available in K/N 2.1? If not, do you have suggestions for an alternative approach to handle such cases? Thanks!
e
isn't this is already possible with
Copy code
val arena = Arena()
// then after you're done
arena.clear()
? or to be closer to the Java version
Copy code
@OptIn(ExperimentalNativeApi::class)
val arena = createCleaner(Arena()) {
    it.clear()
}
r
Yeah, using the Cleaner is probably very similar to what Java does. Thanks!
Hi @ephemient, my goal is to: 1. Provide a no-argument constructor that allocates a
GdkRectangle
, passes it to the primary constructor, and sets up a cleaner to free the allocation when the
Rectangle
object is garbage-collected. 2. Ensure the cleaner runs only after the
Rectangle
instance is no longer in use. Here’s the solution I’ve come up with:
Copy code
public class Rectangle(
    val pointer: CPointer<GdkRectangle>,
    private val cleaner: Cleaner? = null
) {
    public constructor() : this(
        run {
            val arena = Arena()
            val ptr = arena.alloc<GdkRectangle>().ptr
            val c = createCleaner(arena) { it.clear() }
            ptr to c
        }
    )

    private constructor(pair: Pair<CPointer<GdkRectangle>, Cleaner>) : this(
        pointer = pair.first,
        cleaner = pair.second
    )
}
I don’t keep a reference to the
Arena
, only to the
Cleaner
. It seems to behave as expected, where the cleaner is run when the
Rectangle
is garbage-collected. Does this approach seem correct to you? Is there anything I’m overlooking with this design?
o
justing passing by: your solution should work fine, but you can have single constructor here, e.g:
Copy code
public class Rectangle {
    private val pointer: CPointer<GdkRectangle>
    private val cleaner: Cleaner

    init {
        val arena = Arena()
        pointer = arena.alloc<GdkRectangle>().ptr
        cleaner = createCleaner(arena, Arena::clear)
    }
}
Or probably you don't even need to use
Arena
as you are allocating just single object, so probably
nativeHeap
should be enough:
Copy code
public class Rectangle {
    private val pointer: CPointer<GdkRectangle> = nativeHeap.alloc<GdkRectangle>().ptr
    private val cleaner: Cleaner = createCleaner(pointer, nativeHeap::free)
}
Minor suggestion from my experience with K/N cleaner: it's might be better to use function references instead of lambdas so that it will be less error-prone as lambda passed to
createCleaner
should not catch any variable which is declared inside enclosing class or otherwise it will be never GC-ed (as written in
createCleaner
kdoc comment)
r
Hi @Oleg Yukhnevich, thank you for your suggestion and for pointing out the benefits of using function references for
createCleaner
! I really appreciate it. However, my requirements involve distinguishing between two cases: 1. When the class allocates its own
GdkRectangle
(via the no-arg constructor), it should free the memory when the class is garbage-collected. 2. When the class is initialized with an externally provided pointer (via a constructor), it should not attempt to free the memory, as it’s managed externally. If I get it right, in your
init
block solution, the allocation and cleaner setup would always run, even when an external pointer is provided. Similarly, with the
nativeHeap
approach, the class would attempt to free the externally managed pointer, which I want to avoid. Let me know if I’ve misunderstood anything in your solution! And if you have some other idea on how to handle this differently from what I got so far. Thanks again for your input! 🙏
o
Okay, I see! Then in this case I think it would probably be better not to have
default
constructor, but setup this initialization logic just in two separate constructors:
Copy code
public class Rectangle {
    private val pointer: CPointer<GdkRectangle>
    private val cleaner: Cleaner?

    public constructor() {
        this.pointer = nativeHeap.alloc<GdkRectangle>().ptr
        this.cleaner = createCleaner(pointer, nativeHeap::free)
    }

    public constructor(pointer: CPointer<GdkRectangle>) {
        this.pointer = pointer
        this.cleaner = null
    }
    
    init { /* shared initialization if needed */ }
}
It looks more similar to your initial code, but without the pair handling stuff and so both constructors expli Though (a matter of taste), I would probably will go here with private default constructor (as in your initial code) and two functions (with logic inside) in companion object like
Rectagle.wrap(pointer)
and
Rectangle.allocate()
- just to add a bit of clarity on use-site that those are two different semantics:
Copy code
public class Rectangle private constructor(
    private val pointer: CPointer<GdkRectangle>,
    private val cleaner: Cleaner?,
) {
    public companion object {
        public fun allocate(): Rectangle {
            val pointer = nativeHeap.alloc<GdkRectangle>().ptr
            return Rectangle(
                pointer = pointer,
                cleaner = createCleaner(pointer, nativeHeap::free)
            )
        }

        public fun wrap(pointer: CPointer<GdkRectangle>): Rectangle {
            return Rectangle(pointer, null)
        }
    }
}
r
yeah make sense, thank you!