Slice is also unsafe, but the doc say Pointer "This is the only unsafe type in Crystal"

def get_slice
  a = StaticArray[1]
  a.to_slice
end

b = get_slice
p! b # dangling pointer
p! b[0] # 0
def get_slice2
  a = IO::Memory.new 10
  a << "hello"
  a.to_slice
end

b = get_slice2
GC.collect # gc does not work ?
p! b
p! String.new b # "hello"

I wouldn’t go so far as to say “Slice is unsafe” here. Slice on its own isn’t inherently unsafe. You can pass any pointer you like for the buffer, so if you pass a pointer to stack-allocated memory (which is what StaticArray#to_slice is doing), that is what is making it unsafe. Maybe there should be a warning in the StaticArray#to_slice method documentation that warns you to only use that slice within (or downstream of) the method it’s created in.

If you need to return it from the method, use it in a captured block, or any place where you can’t guarantee it’ll be used while the current stack frame is still in scope, don’t use StaticArray#to_slice — use a heap-allocated one instead. Those are safe because Crystal can make guarantees about heap memory that it can’t about stack memory.

It’s working, it’s just that you’re holding a reference to whatever you’re expecting to have been collected. It traverses pointer graphs, so if you’re expecting something to be collected and it isn’t, there may still be a transitive reference to it. But you can see that it does release the memory if you run it in a loop. If you want to be able to see when an object is collected, check out WeakRef.

1 Like

i update it to weak_ref, but still gc not work

require "weak_ref"

def get_slice2
  a = WeakRef.new(IO::Memory.new)
  a.value.not_nil! << "hello" 
  a.value.not_nil!.to_slice
end

b = get_slice2
GC.collect
p! b
p! String.new b  # "hello"

maybe the memory not be overrided?

The GC does not necessarily collect the IO::Memory and its buffer at the same time. It can if the IO::Memory is the only object in your entire program that contains a reference to its buffer, but in your code there are 2 references to it — one in the IO::Memory and one in the Slice.

It works kinda like this:

class IO::Memory
  def to_slice
    Slice(UInt8).new(@buffer, bytesize)
  end
end

struct Slice(T)
  def initialize(@buffer : Pointer(T), @size : Int32)
  end
end

Notice that to_slice passes the buffer pointer to the Slice. This is what’s happening in your code, and that’s what’s preventing the GC from collecting the buffer. It’s not a bug in the GC. The GC is doing exactly what it should in this scenario.

I misunderstood something myself here, as well. WeakRef won’t actually help you visualize this because Pointers are allocated on the stack. That means a WeakRef wrapping a Pointer will never have a nil value because the pointer instance is stored entirely within the WeakRef, not on the heap. So I led you down the wrong path there.

in your code there are 2 references to it — one in the IO::Memory and one in the Slice .

Pointer will inc reference count ?

pointer assigned by GC.malloc_atomic will increase reference count when copy ?

Crystal’s GC doesn’t use reference counting. It’s more like persistence by reachability, so any reference to GC-allocated memory as it traverses the heap will prevent the object at that memory location from being collected.

So yes, passing Pointers around will prevent GC.collect from collecting them. Otherwise, you’ll run into use-after-free issues.

1 Like

All pointers allocated by Crystal’s GC, so pointers returned from both malloc and malloc_atomic will be managed by the GC and any pointers to those buffers will prevent them from being GCed.

The difference is whether the GC looks inside those buffers for more pointers. The GC will look inside malloc pointers for other GC-allocated pointers. It will ignore the contents of anything allocated with malloc_atomic, but will still manage the buffer itself.

It’s like whether you iterate over an array or deal with the array as an opaque container.

Thank you, I finally understand.