2
votes

I'm trying to get my head around FFI in ruby. Is there no way to make use of the return from an FFI callback?

Here's my minimal example:

require 'ffi'

class Foo
  extend FFI::Library
  ffi_lib File.expand_path('fun.o')
  callback :incoming_rpc, [:string], :string
  attach_function :do_some_work, [:incoming_rpc, :string], :string, blocking: true

  def initialize
    @callback = build_callback_runner
    output = do_some_work(@callback, "Ruby init...")
    puts "Output: #{output.inspect}"
  end

  def build_callback_runner
    FFI::Function.new(:string, [:string]) do |name|
      puts "Inside runner: #{name}"
      "DO YOU READ ME?"
    end
  end

end

Foo.new

Here is the C function I'm calling: yep, I know it's not brilliant C, my original receiver was in go, which also fails in exactly the same way. (I won't promise I'm writing brilliant go, either)

// Name: fun.c
// Compiled with: gcc -shared -o fun.o fun.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef char* (*callbkfn)(char*);

extern char* do_some_work(callbkfn fn, char* name);

char* do_some_work(callbkfn fn, char* userdata) {
  printf("do_some_work param: %s\n", userdata);
  printf("callback output: %s\n", fn(strdup("Hello from C")));
  return strdup("Returned from C");
}

Output:

do_some_work param: Ruby init...
Inside runner: Hello from C
callback output: (null)
Output: "Returned from C"

It's that null in the callback output I can't seem to shake. How do you pass the "return" value of a callback FFI::Function or callback proc back into C? The FFI documentation always seems to set callbacks as :void, and I'm guessing the answer is somewhere on the pointers page - but I'm drawing a blank (much like my callback)

2
@Ja͢ck one obviously cannot return strings (whatever it would mean in the c world,) from programs. OS has to clean up the stack once the program has exited, otherwise, each time you run a program, you’ll experience a memory leak. And OS cannot clean up the stack of unknown size, as if char * was allowed as a return value. That’s why e.g. all shell utilities use return codes, not return anything. - Aleksei Matiushkin
@AlekseiMatiushkin this isn’t returning the string from a program, so the c code would still have an opportunity to free() the memory before exit - Ja͢ck
And thanks @Ja͢ck - I don't expect the FFI library to police memory leaks, but you might be right - just because I don't expect them to, doesn't mean they are not. Sounds more like a documentation hole or maybe a missed feature (though I doubt I'm the only person to have come up against this). Oh well. Thanks again all! - user208769
I was curious about this, so did some investigation. The code that wraps the Proc and converts the return value from a Ruby type to a C type doesn’t have an entry for strings, so it falls through to the default case which just sets it to 0... - matt
It’s fairly easy to hack in an extra case here to return a char *, and while that seems to work I don’t know the details of Ruby or FFI enough to know if that would be safe with GC etc. - matt

2 Answers

1
votes

You can get this to work if you use a :pointer as the return type rather than :string (which is just a pointer to char anyway):

class Foo
  extend FFI::Library
  ffi_lib File.expand_path('fun.o')
  callback :incoming_rpc, [:string], :pointer # <- change here
  attach_function :do_some_work, [:incoming_rpc, :string], :string, blocking: true

  def initialize
    @callback = build_callback_runner
    output = do_some_work(@callback, "Ruby init...")
    puts "Output: #{output.inspect}"
  end

  def build_callback_runner
    FFI::Function.new(:pointer, [:string]) do |name|  # <- and here
      puts "Inside runner: #{name}"
      FFI::MemoryPointer.from_string("DO YOU READ ME?") # <- and here
    end
  end

end

Foo.new

With the C code from the question, this produces:

do_some_work param: Ruby init...
Inside runner: Hello from C
callback output: DO YOU READ ME?
Output: "Returned from C"

The main issue is memory allocation and freeing. In this case the MemoryPointer is subject to garbage collection which will free the string data. I’m fairly sure this is safe as long as the C function doesn’t save the pointer and try to use it later (i.e. GC won’t occur before the C function has completed). If it does you will need to ensure that the string is copied by the function, or look into wrapping and using malloc and free.

0
votes

As mentioned in the comments, looks like this feature is missing from the FFI library (boo)

But it's fairly easy to fix with a pointer to a pointer:

// Name: fun.c
// Compiled with: gcc -shared -o fun.o fun.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef char* (*callbkfn)(char**, char*); // note the char**

extern char* do_some_work(callbkfn fn, char* name);

char* do_some_work(callbkfn fn, char* userdata) {
  char* callback_output;
  printf("do_some_work param: %s\n", userdata);
  fn(&callback_output, strdup("Hello from C"));
  printf("callback output: %s\n", callback_output);
  return strdup("Returned from C");
}

Then change the address of the pointer in rubyland:

require 'ffi'

class Foo
  extend FFI::Library
  ffi_lib File.expand_path('fun.o')
  callback :incoming_rpc, [:pointer, :string], :void
  attach_function :do_some_work, [:incoming_rpc, :string], :string, blocking: true

  def initialize
    @callback = build_callback_runner
    output = do_some_work(@callback, "Ruby init...")
    puts "Output: #{output.inspect}"
  end

  def build_callback_runner
    FFI::Function.new(:void, [:pointer, :string]) do |output, input|
      puts "Inside runner: #{input}"
      new_string = FFI::MemoryPointer.from_string("This is my output: #{input.reverse}")
      output.write_pointer new_string.address
    end
  end

end

Foo.new

Again, not sure about what memory leaks this might cause, or whether the GC will make that memorypointer address invalid - will go and do my research now!