how to receive a Ruby Hash created in C in a Ruby Callback via FFI

7 views
Skip to first unread message

Martin Kufner

unread,
Sep 11, 2024, 7:01:41 AM9/11/24
to ruby-ffi
Hey, I was quite good at C 20 years ago, unfortunately I forgot a lot.

In a C function I create a ruby Hash which I want to process in the callback function in Ruby via FFI.

I can trigger the callback with the example below, however, I cannot wrap my brains around how to get the hash in Ruby.

When I execute the code sample below, I get e.g.
Callback #<FFI::Pointer address=0x000000011f414270> ... (the puts statement)
`_id2ref': "69337096" is not id value (RangeError)

Question 1: How do I get this Ruby Hash from C to the callback function
Question 2: Finally the trigger function should called from a C pthread. When I do this it hangs and does not reach the puts "Callback... within the CALLBACK_FFI

What do I do wrong ?

Thanks a lot!
Martin

in Ruby
module QbGameServer
extend FFI::Library
ffi_lib "#{ROOT}/include/qb-game-server.so"
  CALLBACK_FFI = FFI::Function.new(:void, [:pointer]) do |value_ptr|
    puts "Callback #{value_ptr.inspect}"  
    value = FFI::Pointer.new(value_ptr).address
    ruby_hash = ObjectSpace._id2ref(value)
    puts "Received Ruby hash from client data: #{ruby_hash.inspect}"
  end

  callback :ruby_callback_t, %i[pointer], :void
  attach_function :trigger, %i[ruby_callback_t], :void #, blocking: true

  def self.run
    trigger(CALLBACK_FFI)
  end
end
In C (qb-game-server.h excerpt)
void trigger(ruby_callback_t callback) {
  ruby_init();
  ruby_init_loadpath();
  rb_hash_aset(hash, ID2SYM(rb_intern("foo")), rb_str_new2("bar"));
  rb_hash_aset(hash, ID2SYM(rb_intern("baz")), INT2NUM(42));

  // I dont know if this is necessary
  rb_gc_register_address(&hash);

  // Version 1
  callback(hash);

  // Version 2
  callback((void*)hash);

  rb_gc_unregister_address(&hash);
}

Lars Kanis

unread,
Sep 11, 2024, 3:54:38 PM9/11/24
to ruby...@googlegroups.com, Martin Kufner

Hi Martin!

This is a very good question! I also stumbled upon this issue in the past.

Unfortunately you can't pass a CRuby VALUE pointer to ruby space per FFI. This is because FFI is meant to be ruby VM agnostic. Even though it would be very simple, it's not built in, since it wouldn't work with JRuby.

You have two ways to get the key value pairs to ruby space:

1. You convert the ruby hash object per rb_funcall() to a String that can be sent to ruby. For instance call "to_s" on the hash, pass it as a C-string to ruby and use "eval" to get a copy of the Hash. Or you call "Marshal.dump" with the Hash and pass the resulting binary String as a Struct<Pointer,Length> to ruby and use "Marshal.load" to get a copy of the Hash.

2. You build a C representation of the Hash - not using any rb_* functions. Like an Array<Struct<Key,Value>> or so. Then you can pass the Array pointer to ruby and construct the Hash by reading out your corresponding FFI::Struct's or so in Ruby space.

Hope this helps.

--

Regards, Lars

Am 11.09.24 um 11:22 schrieb Martin Kufner:
--
You received this message because you are subscribed to the Google Groups "ruby-ffi" group.
To unsubscribe from this group and stop receiving emails from it, send an email to ruby-ffi+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/ruby-ffi/4e2e52ef-792e-4848-b2be-9d93cfd20222n%40googlegroups.com.

Lars Kanis

unread,
Sep 12, 2024, 2:19:16 AM9/12/24
to ruby...@googlegroups.com, Martin Kufner
Thinking a bit more about this, I guess that you're just missing the "rb_funcall(hash, rb_intern("object_id"),0)" to retrieve the object id, pass it to ruby space and make use of "ObjectSpace._id2ref" to get the corresponding value object back.

In C you can use "RB_GC_GUARD(hash)" at the place where you call "rb_gc_unregister_address" now. Then you don't have to register the object to protect it to be GC'ed.

You can also call "trigger" directly with a block. 

--

Regards, Lars 
Reply all
Reply to author
Forward
0 new messages