summaryrefslogtreecommitdiff
path: root/test/fiddle/test_function.rb
blob: df632925adec73756f52b690d09935fb2e1d549f (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
# 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 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.new(Closure) {
        def call one
          10 + one
        end
      }.new(TYPE_INT, [TYPE_INT])
      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

    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
        skip '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
      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

    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)