summaryrefslogtreecommitdiff
path: root/test/fiddle/test_function.rb
blob: 847df3793ab168645d4fff4106bcf46c7bcbd627 (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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# frozen_string_literal: true
begin
  require_relative 'helper'
rescue LoadError
end

module Fiddle
  class TestFunction < Fiddle::TestCase
    def setup
      super
      Fiddle.last_error = nil
      if WINDOWS
        Fiddle.win32_last_error = nil
        Fiddle.win32_last_socket_error = nil
      end
    end

    def teardown
      # Ensure freeing all closures.
      # See https://github.com/ruby/fiddle/issues/102#issuecomment-1241763091 .
      not_freed_closures = []
      ObjectSpace.each_object(Fiddle::Closure) do |closure|
        not_freed_closures << closure unless closure.freed?
      end
      assert_equal([], not_freed_closures)
    end

    def test_default_abi
      func = Function.new(@libm['sin'], [TYPE_DOUBLE], TYPE_DOUBLE)
      assert_equal Function::DEFAULT, func.abi
    end

    def test_name
      func = Function.new(@libm['sin'], [TYPE_DOUBLE], TYPE_DOUBLE, name: 'sin')
      assert_equal 'sin', func.name
    end

    def test_need_gvl?
      libruby = Fiddle.dlopen(nil)
      rb_str_dup = Function.new(libruby['rb_str_dup'],
                                [:voidp],
                                :voidp,
                                need_gvl: true)
      assert(rb_str_dup.need_gvl?)
      assert_equal('Hello',
                   Fiddle.dlunwrap(rb_str_dup.call(Fiddle.dlwrap('Hello'))))
    end

    def test_argument_errors
      assert_raise(TypeError) do
        Function.new(@libm['sin'], TYPE_DOUBLE, TYPE_DOUBLE)
      end

      assert_raise(TypeError) do
        Function.new(@libm['sin'], ['foo'], TYPE_DOUBLE)
      end

      assert_raise(TypeError) do
        Function.new(@libm['sin'], [TYPE_DOUBLE], 'foo')
      end
    end

    def test_argument_type_conversion
      type = Struct.new(:int, :call_count) do
        def initialize(int)
          super(int, 0)
        end
        def to_int
          raise "exhausted" if (self.call_count += 1) > 1
          self.int
        end
      end
      type_arg = type.new(TYPE_DOUBLE)
      type_result = type.new(TYPE_DOUBLE)
      assert_nothing_raised(RuntimeError) do
        Function.new(@libm['sin'], [type_arg], type_result)
      end
      assert_equal(1, type_arg.call_count)
      assert_equal(1, type_result.call_count)
    end

    def test_call
      func = Function.new(@libm['sin'], [TYPE_DOUBLE], TYPE_DOUBLE)
      assert_in_delta 1.0, func.call(90 * Math::PI / 180), 0.0001
    end

    def test_argument_count
      closure_class = Class.new(Closure) do
        def call one
          10 + one
        end
      end
      closure_class.create(TYPE_INT, [TYPE_INT]) do |closure|
        func = Function.new(closure, [TYPE_INT], TYPE_INT)

        assert_raise(ArgumentError) do
          func.call(1,2,3)
        end
        assert_raise(ArgumentError) do
          func.call
        end
      end
    end

    def test_last_error
      func = Function.new(@libc['strcpy'], [TYPE_VOIDP, TYPE_VOIDP], TYPE_VOIDP)

      assert_nil Fiddle.last_error
      func.call(+"000", "123")
      refute_nil Fiddle.last_error
    end

    if WINDOWS
      def test_win32_last_error
        kernel32 = Fiddle.dlopen("kernel32")
        args = [kernel32["SetLastError"], [-TYPE_LONG], TYPE_VOID]
        args << Function::STDCALL if Function.const_defined?(:STDCALL)
        set_last_error = Function.new(*args)
        assert_nil(Fiddle.win32_last_error)
        n = 1 << 29 | 1
        set_last_error.call(n)
        assert_equal(n, Fiddle.win32_last_error)
      end

      def test_win32_last_socket_error
        ws2_32 = Fiddle.dlopen("ws2_32")
        args = [ws2_32["WSASetLastError"], [TYPE_INT], TYPE_VOID]
        args << Function::STDCALL if Function.const_defined?(:STDCALL)
        wsa_set_last_error = Function.new(*args)
        assert_nil(Fiddle.win32_last_socket_error)
        n = 1 << 29 | 1
        wsa_set_last_error.call(n)
        assert_equal(n, Fiddle.win32_last_socket_error)
      end
    end

    def test_strcpy
      f = Function.new(@libc['strcpy'], [TYPE_VOIDP, TYPE_VOIDP], TYPE_VOIDP)
      buff = +"000"
      str = f.call(buff, "123")
      assert_equal("123", buff)
      assert_equal("123", str.to_s)
    end

    def call_proc(string_to_copy)
      buff = +"000"
      str = yield(buff, string_to_copy)
      [buff, str]
    end

    def test_function_as_proc
      f = Function.new(@libc['strcpy'], [TYPE_VOIDP, TYPE_VOIDP], TYPE_VOIDP)
      buff, str = call_proc("123", &f)
      assert_equal("123", buff)
      assert_equal("123", str.to_s)
    end

    def test_function_as_method
      f = Function.new(@libc['strcpy'], [TYPE_VOIDP, TYPE_VOIDP], TYPE_VOIDP)
      klass = Class.new do
        define_singleton_method(:strcpy, &f)
      end
      buff = +"000"
      str = klass.strcpy(buff, "123")
      assert_equal("123", buff)
      assert_equal("123", str.to_s)
    end

    def test_nogvl_poll
      # XXX hack to quiet down CI errors on EINTR from r64353
      # [ruby-core:88360] [Misc #14937]
      # Making pipes (and sockets) non-blocking by default would allow
      # us to get rid of POSIX timers / timer pthread
      # https://bugs.ruby-lang.org/issues/14968
      IO.pipe { |r,w| IO.select([r], [w]) }
      begin
        poll = @libc['poll']
      rescue Fiddle::DLError
        omit 'poll(2) not available'
      end
      f = Function.new(poll, [TYPE_VOIDP, TYPE_INT, TYPE_INT], TYPE_INT)

      msec = 200
      t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
      th = Thread.new { f.call(nil, 0, msec) }
      n1 = f.call(nil, 0, msec)
      n2 = th.value
      t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
      assert_in_delta(msec, t1 - t0, 180, 'slept amount of time')
      assert_equal(0, n1, perror("poll(2) in main-thread"))
      assert_equal(0, n2, perror("poll(2) in sub-thread"))
    end

    def test_no_memory_leak
      if respond_to?(:assert_nothing_leaked_memory)
        rb_obj_frozen_p_symbol = Fiddle.dlopen(nil)["rb_obj_frozen_p"]
        rb_obj_frozen_p = Fiddle::Function.new(rb_obj_frozen_p_symbol,
                                               [Fiddle::TYPE_UINTPTR_T],
                                               Fiddle::TYPE_UINTPTR_T)
        a = "a"
        n_tries = 100_000
        n_tries.times do
          begin
            a + 1
          rescue TypeError
          end
        end
        n_arguments = 1
        sizeof_fiddle_generic = Fiddle::SIZEOF_VOIDP # Rough
        size_per_try =
          (sizeof_fiddle_generic * n_arguments) +
          (Fiddle::SIZEOF_VOIDP * (n_arguments + 1))
        assert_nothing_leaked_memory(size_per_try * n_tries) do
          n_tries.times do
            begin
              rb_obj_frozen_p.call(a)
            rescue TypeError
            end
          end
        end
      else
        prep = 'r = Fiddle::Function.new(Fiddle.dlopen(nil)["rb_obj_frozen_p"], [Fiddle::TYPE_UINTPTR_T], Fiddle::TYPE_UINTPTR_T); a = "a"'
        code = 'begin r.call(a); rescue TypeError; end'
        assert_no_memory_leak(%w[-W0 -rfiddle], "#{prep}\n1000.times{#{code}}", "10_000.times {#{code}}", limit: 1.2)
      end
    end

    private

    def perror(m)
      proc do
        if e = Fiddle.last_error
          m = "#{m}: #{SystemCallError.new(e).message}"
        end
        m
      end
    end
  end
end if defined?(Fiddle)