Subject:
Re: [ruby-ffi] Struct/ManagedStruct and GC behaviour
From:
Wayne Meissner
Date:
12/21/09 4:00 PM
To:
ruby-ffi@googlegroups.com

2009/12/22 Jon <jon.forums@gmail.com>:
> Before I add API-ish level doc to http://wiki.github.com/ffi/ffi/structs please double check my FFI::Struct and FFI::ManagedStruct understanding.  Please ensure feedback is valid across the impl's.

A ManagedStruct is basically a normal Struct with an AutoPointer to
manage the backing memory.
e.g. it is equivalent to:

class XmlSandwichAutoPointer < FFI::AutoPointer
  def self.release(ptr)
    LibXml.xmlFreeSandwich(ptr);
  end
end

class XmlSandwichStruct < FFI::Struct
  layout ...
end

s = XmlSandwichStruct.new(XmlSandwichAutoPointer.new(LibXml.makeMeASandwich)

Since ManagedStruct is implemented purely in terms of other public FFI
mechanisms, and it can be a bit confusing for people, it would be a
prime candidate for being moved out of FFI core, to another library.


> 4) FFI::Struct and FFI::ManagedStruct instances live completely in Ruby heap memory.  Or are they effectively proxy objects in Ruby heap that interface and manage their underlying native heap-based structs? This one's likely too impl specific to put in the wiki docs, but I'm still curious :)

Both FFI::Struct and FFI::ManagedStruct just hold ruby references to
whatever memory object they wrap.  That memory object is the ruby
proxy that manages the underlying native memory.  So yes, they are
effectively proxies (via the memory object) to native heap-based
structs.

Since a Struct is holding a normal ruby reference to the memory proxy,
then all the normal garbage collection semantics apply.  i.e. as long
as there is at least one strong reference to the memory proxy, the
native memory will not be freed.


Some implementation level detail examples.

Assume we have a mapping like:

module LibXml
  attach_function :xmlMakeMeASandwich [], :pointer
  attach_function :xmlFreeSandwich [ :pointer ]
end

and we defined the XmlSandwichAutoPointer class as previously.

Passive memory (not managed at all by ruby)

  ptr = LibXml.makeMeASandwich
  s = XmlSandwichStruct.new(ptr)
  ptr = nil
  s = nil

  # s can now be collected, ptr can now be collected, but the native
memory is not freed, since
  # ptr was a purely passive pointer

Actively managed memory (obtained from function call)

  ptr = XmlSandwichStruct.new(LibXml.makeMeASandwich)
  s = XmlSandwichStruct.new(XmlSandwichAutoPointer.new(ptr))
  s = nil
  # s can now be collected, the backing AutoPointer instance can be
collected, which
  # will call xmlFreeSandwich()


Actively managed memory (allocated internally by FFI)

  s = XmlSandwichStruct.new
  s = nil
  # s can now be collected, the backing memory proxy can be collected,
which will free the
  # allocated native memory.

If you drill down further on this one, it is equivalent to:

  ptr = MemoryPointer.new(XmlSandwichStruct.size)
  # we have allocated some native memory that is managed by 'ptr'
  s = XmlSandwichStruct.new(ptr)

  ptr = nil
  # s still has a ref to ptr, so the memory ptr manages will not be freed

  s = nil
  # s can now be collected, the backing memory proxy can be collected,
which will free the
  # allocated native memory.

Since it all follows normal ruby GC semantics, stuff like this is valid:

  ptr = MemoryPointer.new(XmlSandwichStruct.size)
  # we have allocated some native memory that is managed by 'ptr'
  s = XmlSandwichStruct.new(ptr)
  ...
  s = nil
  # local frame still has a ref to ptr, therefore ptr will not be
collected, and the
  # memory ptr manages will not be freed yet

  ptr.get_int(0)
  # ... do other things using ptr

  ptr = nil
  # ptr can now be collected, enabling the native memory to be freed


Or even this:
  s = XmlSandwichStruct.new
  ptr = s.pointer
  s = nil
  ptr.get_int(0)
  ptr = nil
  # ptr can now be collected, enabling the native memory to be freed


Hopefully the above isn't even more confusing.

Note: I purposely did not discuss using a FFI::Buffer as FFI::Struct
memory, since that could have confused the explanation of the native
memory management.  The same same ruby GC semantics apply, of course.

> 3) Neither FFI::Struct nor FFI::ManagedStruct currently provide for deterministic GC-ing (is it valid to call FFI::MemoryPointer#free deterministic GC??)

That depends on the type of memory the Struct wraps.

e.g. if it wraps either a MemoryPointer or an AutoPointer, then this
is perfectly valid:

 s.pointer.free

Of course, once you do that, s is now wrapping a dangling pointer, and
if you pass it to a native function that tries to access it, fun and
excitement will ensue.

If s.pointer refers to a non-managed pointer, then it has no way to
manage the memory, so s.pointer.free will raise a ruby exception.

> 2) FFI::ManagedStruct enables one to specify custom cleanup code via a required 'release(ptr)' class method that is guaranteed to be called only once when the instance is GC'd.

Correct.  AutoPointer#free will only call the release method once.