summaryrefslogtreecommitdiff
path: root/spec/ffi/function_spec.rb
blob: 77d09d887d451ad8915f312754237d74a9b2073d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#
# This file is part of ruby-ffi.
# For licensing, see LICENSE.SPECS
#

require File.expand_path(File.join(File.dirname(__FILE__), "spec_helper"))

describe FFI::Function do
  module LibTest
    extend FFI::Library
    ffi_lib TestLibrary::PATH
    attach_function :testFunctionAdd, [:int, :int, :pointer], :int
  end
  before do
    @libtest = FFI::DynamicLibrary.open(TestLibrary::PATH,
                                        FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_GLOBAL)
  end

  it 'is initialized with a signature and a block' do
    fn = FFI::Function.new(:int, []) { 5 }
    expect(fn.call).to eql 5
  end

  context 'when called with a block' do
    it 'creates a thread for dispatching callbacks and sets its name' do
      skip 'this is MRI-specific' if RUBY_ENGINE == 'truffleruby' || RUBY_ENGINE == 'jruby'
      FFI::Function.new(:int, []) { 5 } # Trigger initialization

      expect(Thread.list.map(&:name)).to include('FFI Callback Dispatcher')
    end
  end

  it 'raises an error when passing a wrong signature' do
    expect { FFI::Function.new([], :int).new { } }.to raise_error TypeError
  end

  it 'returns a native pointer' do
    expect(FFI::Function.new(:int, []) { }).to be_a_kind_of FFI::Pointer
  end

  it 'can be used as callback from C passing to it a block' do
    function_add = FFI::Function.new(:int, [:int, :int]) { |a, b| a + b }
    expect(LibTest.testFunctionAdd(10, 10, function_add)).to eq(20)
  end

  it 'can be used as callback from C passing to it a Proc object' do
    function_add = FFI::Function.new(:int, [:int, :int], Proc.new { |a, b| a + b })
    expect(LibTest.testFunctionAdd(10, 10, function_add)).to eq(20)
  end

  def adder(a, b)
    a + b
  end

  it "can be made shareable for Ractor", :ractor do
    add = FFI::Function.new(:int, [:int, :int], &method(:adder))
    Ractor.make_shareable(add)

    res = Ractor.new(add) do |add2|
      LibTest.testFunctionAdd(10, 10, add2)
    end.take

    expect( res ).to eq(20)
  end

  it "should be usable with Ractor", :ractor do
    res = Ractor.new do
      function_add = FFI::Function.new(:int, [:int, :int]) { |a, b| a + b }
      LibTest.testFunctionAdd(10, 10, function_add)
    end.take

    expect( res ).to eq(20)
  end

  it 'can be used to wrap an existing function pointer' do
    expect(FFI::Function.new(:int, [:int, :int], @libtest.find_function('testAdd')).call(10, 10)).to eq(20)
  end

  it 'can be attached to a module' do
    module Foo; end
    fp = FFI::Function.new(:int, [:int, :int], @libtest.find_function('testAdd'))
    fp.attach(Foo, 'add')
    expect(Foo.add(10, 10)).to eq(20)
  end

  it 'can be attached to two modules' do
    module Foo1; end
    module Foo2; end
    fp = FFI::Function.new(:int, [:int, :int], @libtest.find_function('testAdd'))
    fp.attach(Foo1, 'add')
    fp.attach(Foo2, 'add')
    expect(Foo1.add(11, 11)).to eq(22)
    expect(Foo2.add(12, 12)).to eq(24)
  end

  it 'can be used to extend an object' do
    fp = FFI::Function.new(:int, [:int, :int], @libtest.find_function('testAdd'))
    foo = Object.new
    class << foo
      def singleton_class
        class << self; self; end
      end
    end
    fp.attach(foo.singleton_class, 'add')
    expect(foo.add(10, 10)).to eq(20)
  end

  it 'can wrap a blocking function' do
    fpOpen = FFI::Function.new(:pointer, [ ], @libtest.find_function('testBlockingOpen'))
    fpRW = FFI::Function.new(:char, [ :pointer, :char ], @libtest.find_function('testBlockingRW'), :blocking => true)
    fpWR = FFI::Function.new(:char, [ :pointer, :char ], @libtest.find_function('testBlockingWR'), :blocking => true)
    fpClose = FFI::Function.new(:void, [ :pointer ], @libtest.find_function('testBlockingClose'))
    handle = fpOpen.call
    expect(handle).not_to be_null
    begin
      thWR = Thread.new { fpWR.call(handle, 63) }
      thRW = Thread.new { fpRW.call(handle, 64) }
      expect(thWR.value).to eq(64)
      expect(thRW.value).to eq(63)
    ensure
      fpClose.call(handle)
    end
  end

  it 'autorelease flag is set to true by default' do
    fp = FFI::Function.new(:int, [:int, :int], @libtest.find_function('testAdd'))
    expect(fp.autorelease?).to be true
  end

  it 'can explicity free itself' do
    fp = FFI::Function.new(:int, []) { }
    fp.free
    expect { fp.free }.to raise_error RuntimeError
  end

  it 'can\'t explicity free itself if not previously allocated' do
    fp = FFI::Function.new(:int, [:int, :int], @libtest.find_function('testAdd'))
    expect { fp.free }.to raise_error RuntimeError
  end

  it 'has a memsize function', skip: RUBY_ENGINE != "ruby" do
    base_size = ObjectSpace.memsize_of(Object.new)

    function = FFI::Function.new(:int, [:int, :int], @libtest.find_function('testAdd'))
    size = ObjectSpace.memsize_of(function)
    expect(size).to be > base_size
  end
end