Search code examples
rubygarbage-collectionffi

Question about out-of-scope FFI MemoryPointer that is returned from a method


Let's say I have the following ruby code that uses FFI library:

  class SimpleStruct < FFI::Struct
    layout :value, :pointer
  end
  
  class Test1
    def self.foo(s)
      result = FFI::MemoryPointer.from_string(s)

      result
    end

    def self.bar(s)
      simple_struct = SimpleStruct.new
      value = FFI::MemoryPointer.from_string(s)
      simple_struct[:value] = value

      simple_struct
    end
  end
  
  
  class Test2
    def self.testing
      a = Test1.foo('test')
      b = Test1.bar('test')

      puts a.read_string, b[:value].read_string
    end
  end

The FFI wiki mentions When a MemoryPointer goes out of scope, the memory is freed up as part of the garbage collection process. In the Test1 class above, foo method returns a MemoryPointer and bar method returns an FFI struct that holds MemoryPointer. The testing method in Test2 class calls these methods and stores the returned values in variable a and b respectively.

My question is when the MemoryPointer created inside foo and bar method will be garbage collected in this case? Is it when the MemoryPointer variable goes out of scope in foo and bar methods or when the local variable a or b (that references the MemoryPointer returned from the foo/bar methods) goes out of scope in testing method?


Solution

  • I believe FFI::Struct#[]= takes care of this. I didn't check it in the sources, but I've added some checks into your code and it looks so.

    require 'ffi'
    
    class SimpleStruct < FFI::Struct
      layout :value, :pointer
    end
    
    class Test1
      def self.foo(s)
        puts "foo is called"
        result = FFI::MemoryPointer.from_string(s)
        ObjectSpace.define_finalizer(result, proc { puts "foo result is garbage collected" })
        result
      end
    
      def self.bar(s)
        puts "bar is called"
        simple_struct = SimpleStruct.new
        ObjectSpace.define_finalizer(s, proc { puts "bar result is garbage collected" })
        value = FFI::MemoryPointer.from_string(s)
        ObjectSpace.define_finalizer(s, proc { puts "bar result[:value] is garbage collected" })
        simple_struct[:value] = value
    
        simple_struct
      end
    
      def self.baz(s)
        puts "baz is called"
        simple_struct = {}
        ObjectSpace.define_finalizer(s, proc { puts "baz result is garbage collected" })
        value = FFI::MemoryPointer.from_string(s)
        ObjectSpace.define_finalizer(s, proc { puts "baz result[:value] is garbage collected" })
        simple_struct[:value] = value
    
        simple_struct
      end
    end
    
    class Test2
      def self.testing
        puts "testing is started"
        a = Test1.foo('foo')
        b = Test1.bar('bar')
        c = Test1.baz('baz')
    
        puts a.read_string, b[:value].read_string, c[:value].read_string
        puts "testing is finished"
      end
    end
    
    GC.stress
    
    Test2.testing
    
    # testing is started
    # foo is called
    # bar is called
    # baz is called
    # foo
    # bar
    # baz
    # testing is finished
    # baz result is garbage collected
    # baz result[:value] is garbage collected
    # bar result is garbage collected
    # bar result[:value] is garbage collected
    # foo result is garbage collected
    

    A pointer is only garbage collected when the struct is garbage-collected, as is the case with a hash.