From ebad6efb096bea19287b8c495721ded5b6035596 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Sat, 28 Sep 2024 06:59:21 +0900 Subject: [PATCH] Import JRuby implementation Fig GH-104 lib/fiddle/jruby.rb is based on https://github.com/jruby/jruby/blob/master/lib/ruby/stdlib/fiddle/jruby.rb . Here are changes for it: * Move `Fiddle::TYPE_*` to `Fiddle::Types::*` * Add `Fiddle::Types::VARIADIC` * Add `Fiddle::Types::CONST_STRING` * Add `Fiddle::Types::BOOL` * Add `Fiddle::Types::INT8_T` * Add `Fiddle::Types::UINT8_T` * Add `Fiddle::Types::INT16_T` * Add `Fiddle::Types::UINT16_T` * Add `Fiddle::Types::INT32_T` * Add `Fiddle::Types::UINT32_T` * Add `Fiddle::Types::INT64_T` * Add `Fiddle::Types::UINT64_T` * Add more `Fiddle::ALIGN_*` for the above new `Fiddle::Types::*` * Add more `Fiddle::SIZEOF_*` for the above new `Fiddle::Types::*` * Add support for specifying type as not only `Fiddle::Types::*` but also `Symbol` like `:int` * Add support for variable size arguments in `Fiddle::Function` * Add `Fiddle::Closure#free` * Add `Fiddle::Closure#freed?` * Add `Fiddle::Error` as base the error class * Add `Fiddle::Pointer#call_free` and stop using `FFI::AutoPointer` in `Fiddle::Pointer` * Add support for `Fiddle::Pointer.malloc {}` `Fiddle::Pointer` * Add support for `Fiddle::Pointer#free=` * Add `Fiddle::Pointer#freed?` * Use binary string not C string for `Fiddle::Pointer#[]` * Add `Fiddle::Handle.sym_defined?` * Add `Fiddle::Handle#sym_defined?` * Add `Fiddle::Handle::DEFAULT` * Add `Fiddle::ClearedReferenceError` * Add no-op `Fiddle::Pinned` Some features are still "not implemented". So there are some "omit"s for JRuby in tests. --- .github/workflows/ci.yml | 10 +- Rakefile | 13 +- dockerfiles/fedora-latest.dockerfile | 4 +- ext/fiddle/extconf.rb | 5 + fiddle.gemspec | 1 + lib/fiddle.rb | 48 ++- lib/fiddle/jruby.rb | 600 +++++++++++++++++++++++++++ lib/fiddle/struct.rb | 33 +- test/fiddle/helper.rb | 11 +- test/fiddle/test_c_struct_entry.rb | 5 + test/fiddle/test_c_union_entity.rb | 5 + test/fiddle/test_closure.rb | 31 +- test/fiddle/test_fiddle.rb | 4 + test/fiddle/test_func.rb | 59 +-- test/fiddle/test_function.rb | 26 ++ test/fiddle/test_handle.rb | 30 +- test/fiddle/test_import.rb | 15 +- test/fiddle/test_pointer.rb | 39 +- test/run.rb | 21 +- 19 files changed, 871 insertions(+), 89 deletions(-) create mode 100644 lib/fiddle/jruby.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9941930..05f5f249 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,7 @@ jobs: - '3.1' - '3.2' - debug + - jruby include: - { os: windows-latest , ruby: mingw } - { os: windows-latest , ruby: mswin } @@ -53,13 +54,9 @@ jobs: matrix.ruby == 'mingw' || matrix.ruby == 'mswin' - - run: rake build + - run: ruby -Ilib test/run.rb - - run: gem install pkg/*.gem - - - run: rake test - env: - RUBYOPT: --disable=gems + - run: rake install - name: Run test against installed gem # We can't use Fiddle gem with RubyInstaller because @@ -69,6 +66,7 @@ jobs: matrix.os != 'windows-latest' || (matrix.os == 'windows-latest' && matrix.ruby == 'mswin') run: | + ruby -run -e mkdir -- -p tmp/ ruby -run -e cp -- -pr test/ tmp/ cd tmp ruby test/run.rb diff --git a/Rakefile b/Rakefile index aefc9ed5..01d8ccff 100644 --- a/Rakefile +++ b/Rakefile @@ -17,8 +17,13 @@ namespace :version do end end -require 'rake/extensiontask' -Rake::ExtensionTask.new("fiddle") -Rake::ExtensionTask.new("-test-/memory_view") +if RUBY_ENGINE == "ruby" + require 'rake/extensiontask' + Rake::ExtensionTask.new("fiddle") + Rake::ExtensionTask.new("-test-/memory_view") + task test: :compile +else + task :compile +end -task :default => [:compile, :test] +task default: :test diff --git a/dockerfiles/fedora-latest.dockerfile b/dockerfiles/fedora-latest.dockerfile index 49163ad0..2f376925 100644 --- a/dockerfiles/fedora-latest.dockerfile +++ b/dockerfiles/fedora-latest.dockerfile @@ -12,7 +12,9 @@ RUN \ dnf clean all RUN \ - gem install bundler + gem install \ + test-unit \ + test-unit-ruby-core RUN \ useradd --user-group --create-home user diff --git a/ext/fiddle/extconf.rb b/ext/fiddle/extconf.rb index 2d85b3ee..6b0ea753 100644 --- a/ext/fiddle/extconf.rb +++ b/ext/fiddle/extconf.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true require 'mkmf' +if RUBY_ENGINE == "jruby" + File.write('Makefile', dummy_makefile("").join) + return +end + # :stopdoc: def gcc? diff --git a/fiddle.gemspec b/fiddle.gemspec index fc3cbfab..309cf7b5 100644 --- a/fiddle.gemspec +++ b/fiddle.gemspec @@ -40,6 +40,7 @@ Gem::Specification.new do |spec| "lib/fiddle/cparser.rb", "lib/fiddle/function.rb", "lib/fiddle/import.rb", + "lib/fiddle/jruby.rb", "lib/fiddle/pack.rb", "lib/fiddle/struct.rb", "lib/fiddle/types.rb", diff --git a/lib/fiddle.rb b/lib/fiddle.rb index 6137c487..9e19fc6f 100644 --- a/lib/fiddle.rb +++ b/lib/fiddle.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true -require 'fiddle.so' +case RUBY_ENGINE +when 'jruby' + require 'fiddle/jruby' +else + require 'fiddle.so' +end require 'fiddle/closure' require 'fiddle/function' require 'fiddle/version' @@ -10,36 +15,63 @@ module Fiddle # Returns the last win32 +Error+ of the current executing +Thread+ or nil # if none def self.win32_last_error - Thread.current[:__FIDDLE_WIN32_LAST_ERROR__] + if RUBY_ENGINE == 'jruby' + errno = FFI.errno + errno == 0 ? nil : errno + else + Thread.current[:__FIDDLE_WIN32_LAST_ERROR__] + end end # Sets the last win32 +Error+ of the current executing +Thread+ to +error+ def self.win32_last_error= error - Thread.current[:__FIDDLE_WIN32_LAST_ERROR__] = error + if RUBY_ENGINE == 'jruby' + FFI.errno = error || 0 + else + Thread.current[:__FIDDLE_WIN32_LAST_ERROR__] = error + end end # Returns the last win32 socket +Error+ of the current executing # +Thread+ or nil if none def self.win32_last_socket_error - Thread.current[:__FIDDLE_WIN32_LAST_SOCKET_ERROR__] + if RUBY_ENGINE == 'jruby' + errno = FFI.errno + errno == 0 ? nil : errno + else + Thread.current[:__FIDDLE_WIN32_LAST_SOCKET_ERROR__] + end end # Sets the last win32 socket +Error+ of the current executing # +Thread+ to +error+ def self.win32_last_socket_error= error - Thread.current[:__FIDDLE_WIN32_LAST_SOCKET_ERROR__] = error + if RUBY_ENGINE == 'jruby' + FFI.errno = error || 0 + else + Thread.current[:__FIDDLE_WIN32_LAST_SOCKET_ERROR__] = error + end end end # Returns the last +Error+ of the current executing +Thread+ or nil if none def self.last_error - Thread.current[:__FIDDLE_LAST_ERROR__] + if RUBY_ENGINE == 'jruby' + errno = FFI.errno + errno == 0 ? nil : errno + else + Thread.current[:__FIDDLE_LAST_ERROR__] + end end # Sets the last +Error+ of the current executing +Thread+ to +error+ def self.last_error= error - Thread.current[:__DL2_LAST_ERROR__] = error - Thread.current[:__FIDDLE_LAST_ERROR__] = error + if RUBY_ENGINE == 'jruby' + FFI.errno = error || 0 + else + Thread.current[:__DL2_LAST_ERROR__] = error + Thread.current[:__FIDDLE_LAST_ERROR__] = error + end end # call-seq: dlopen(library) => Fiddle::Handle diff --git a/lib/fiddle/jruby.rb b/lib/fiddle/jruby.rb new file mode 100644 index 00000000..6d432876 --- /dev/null +++ b/lib/fiddle/jruby.rb @@ -0,0 +1,600 @@ +# This is part of JRuby's FFI-based fiddle implementation. + +require 'ffi' + +module Fiddle + def self.malloc(size) + Fiddle::Pointer.malloc(size) + end + + def self.free(ptr) + Fiddle::Pointer::LibC::FREE.call(ptr) + nil + end + + def self.dlwrap(val) + Pointer.to_ptr(val) + end + + module Types + VOID = 0 + VOIDP = 1 + CHAR = 2 + UCHAR = -CHAR + SHORT = 3 + USHORT = -SHORT + INT = 4 + UINT = -INT + LONG = 5 + ULONG = -LONG + LONG_LONG = 6 + ULONG_LONG = -LONG_LONG + FLOAT = 7 + DOUBLE = 8 + VARIADIC = 9 + CONST_STRING = 10 + BOOL = 11 + INT8_T = CHAR + UINT8_T = UCHAR + if FFI::Type::Builtin::SHORT.size == 2 + INT16_T = SHORT + UINT16_T = USHORT + elsif FFI::Type::Builtin::INT.size == 2 + INT16_T = INT + UINT16_T = UINT + end + if FFI::Type::Builtin::SHORT.size == 4 + INT32_T = SHORT + UINT32_T = USHORT + elsif FFI::Type::Builtin::INT.size == 4 + INT32_T = INT + UINT32_T = UINT + elsif FFI::Type::Builtin::LONG.size == 4 + INT32_T = LONG + UINT32_T = ULONG + end + if FFI::Type::Builtin::INT.size == 8 + INT64_T = INT + UINT64_T = UINT + elsif FFI::Type::Builtin::LONG.size == 8 + INT64_T = LONG + UINT64_T = ULONG + else + INT64_T = LONG_LONG + UINT64_T = ULONG_LONG + end + + # FIXME: platform specific values + SSIZE_T = INT64_T + SIZE_T = -SSIZE_T + PTRDIFF_T = SSIZE_T + INTPTR_T = INT64_T + UINTPTR_T = -INTPTR_T + end + + WINDOWS = FFI::Platform.windows? + + module JRuby + FFITypes = { + 'c' => FFI::Type::INT8, + 'h' => FFI::Type::INT16, + 'i' => FFI::Type::INT32, + 'l' => FFI::Type::LONG, + 'f' => FFI::Type::FLOAT32, + 'd' => FFI::Type::FLOAT64, + 'p' => FFI::Type::POINTER, + 's' => FFI::Type::STRING, + + Types::VOID => FFI::Type::Builtin::VOID, + Types::VOIDP => FFI::Type::Builtin::POINTER, + Types::CHAR => FFI::Type::Builtin::CHAR, + Types::UCHAR => FFI::Type::Builtin::UCHAR, + Types::SHORT => FFI::Type::Builtin::SHORT, + Types::USHORT => FFI::Type::Builtin::USHORT, + Types::INT => FFI::Type::Builtin::INT, + Types::UINT => FFI::Type::Builtin::UINT, + Types::LONG => FFI::Type::Builtin::LONG, + Types::ULONG => FFI::Type::Builtin::ULONG, + Types::LONG_LONG => FFI::Type::Builtin::LONG_LONG, + Types::ULONG_LONG => FFI::Type::Builtin::ULONG_LONG, + Types::FLOAT => FFI::Type::Builtin::FLOAT, + Types::DOUBLE => FFI::Type::Builtin::DOUBLE, + Types::BOOL => FFI::Type::Builtin::BOOL, + Types::CONST_STRING => FFI::Type::Builtin::POINTER, + Types::VARIADIC => FFI::Type::Builtin::VARARGS, + } + + def self.__ffi_type__(dl_type) + if dl_type.is_a?(Symbol) + dl_type = Types.const_get(dl_type.to_s.upcase) + end + if !dl_type.is_a?(Integer) && dl_type.respond_to?(:to_int) + dl_type = dl_type.to_int + end + ffi_type = FFITypes[dl_type] + ffi_type = FFITypes[-dl_type] if ffi_type.nil? && dl_type.is_a?(Integer) && dl_type < 0 + raise TypeError.new("cannot convert #{dl_type} to ffi") unless ffi_type + ffi_type + end + end + + class Function + DEFAULT = "default" + STDCALL = "stdcall" + + def initialize(ptr, args, return_type, abi = DEFAULT, kwargs = nil) + if kwargs.nil? + if abi.kind_of? Hash + kwargs = abi + abi = DEFAULT + end + end + @name = kwargs[:name] if kwargs.kind_of? Hash + @ptr, @args, @return_type, @abi = ptr, args, return_type, abi + raise TypeError.new "invalid argument types" unless args.is_a?(Array) + + ffi_return_type = Fiddle::JRuby::__ffi_type__(@return_type) + ffi_args = @args.map { |t| Fiddle::JRuby.__ffi_type__(t) } + pointer = FFI::Pointer.new(ptr.to_i) + options = {convention: @abi} + if ffi_args.last == FFI::Type::Builtin::VARARGS + @function = FFI::VariadicInvoker.new( + pointer, + ffi_args, + ffi_return_type, + options + ) + else + @function = FFI::Function.new(ffi_return_type, ffi_args, pointer, options) + end + end + + def call(*args, &block); + if @function.is_a?(FFI::VariadicInvoker) + n_fixed_args = @args.size - 1 + n_fixed_args.step(args.size - 1, 2) do |i| + if args[i] == :const_string || args[i] == Types::CONST_STRING + args[i + 1] = String.try_convert(args[i + 1]) || args[i + 1] + end + args[i] = Fiddle::JRuby.__ffi_type__(args[i]) + end + end + result = @function.call(*args, &block) + result = Pointer.new(result) if result.is_a?(FFI::Pointer) + result + end + end + + class Closure + def initialize(ret, args, abi = Function::DEFAULT) + raise TypeError.new "invalid argument types" unless args.is_a?(Array) + + @ctype, @args = ret, args + ffi_args = @args.map { |t| Fiddle::JRuby.__ffi_type__(t) } + if ffi_args.size == 1 && ffi_args[0] == FFI::Type::Builtin::VOID + ffi_args = [] + end + @function = FFI::Function.new( + Fiddle::JRuby.__ffi_type__(@ctype), + ffi_args, + self, + :convention => abi + ) + @freed = false + end + + def to_i + @function.to_i + end + + def free + return if @freed + @function.free + @freed = true + end + + def freed? + @freed + end + end + + class Error < StandardError; end + class DLError < Error; end + class ClearedReferenceError < Error; end + + class Pointer + attr_reader :ffi_ptr + extend FFI::DataConverter + native_type FFI::Type::Builtin::POINTER + + def self.to_native(value, ctx) + if value.is_a?(Pointer) + value.ffi_ptr + + elsif value.is_a?(Integer) + FFI::Pointer.new(value) + + elsif value.is_a?(String) + value + end + end + + def self.from_native(value, ctx) + self.new(value) + end + + def self.to_ptr(value) + if value.is_a?(String) + cptr = Pointer.malloc(value.bytesize) + cptr.ffi_ptr.put_string(0, value) + cptr + + elsif value.is_a?(Array) + raise NotImplementedError, "array ptr" + + elsif value.respond_to?(:to_ptr) + ptr = value.to_ptr + case ptr + when Pointer + ptr + when FFI::Pointer + Pointer.new(ptr) + else + raise DLError.new("to_ptr should return a Fiddle::Pointer object, was #{ptr.class}") + end + + else + Pointer.new(value) + end + end + + class << self + alias [] to_ptr + end + + def []=(*args, value) + if args.size == 2 + if value.is_a?(Integer) + value = self.class.new(value) + end + if value.is_a?(Fiddle::Pointer) + value = value.to_str(args[1]) + end + + @ffi_ptr.put_bytes(args[0], value, 0, args[1]) + elsif args.size == 1 + if value.is_a?(Fiddle::Pointer) + value = value.to_str(args[0] + 1) + else + value = value.chr + end + + @ffi_ptr.put_bytes(args[0], value, 0, 1) + end + rescue FFI::NullPointerError + raise DLError.new("NULL pointer access") + end + + def initialize(addr, size = nil, free = nil) + ptr = if addr.is_a?(FFI::Pointer) + addr + + elsif addr.is_a?(Integer) + FFI::Pointer.new(addr) + + elsif addr.respond_to?(:to_ptr) + fiddle_ptr = addr.to_ptr + if fiddle_ptr.is_a?(Pointer) + fiddle_ptr.ffi_ptr + elsif fiddle_ptr.is_a?(FFI::AutoPointer) + addr.ffi_ptr + elsif fiddle_ptr.is_a?(FFI::Pointer) + fiddle_ptr + else + raise DLError.new("to_ptr should return a Fiddle::Pointer object, was #{fiddle_ptr.class}") + end + elsif addr.is_a?(IO) + raise NotImplementedError, "IO ptr isn't supported" + end + + @size = size ? size : ptr.size + @free = free + @ffi_ptr = ptr + @freed = false + end + + module LibC + extend FFI::Library + ffi_lib FFI::Library::LIBC + MALLOC = attach_function :malloc, [ :size_t ], :pointer + REALLOC = attach_function :realloc, [ :pointer, :size_t ], :pointer + FREE = attach_function :free, [ :pointer ], :void + end + + def self.malloc(size, free = nil) + if block_given? and free.nil? + message = "a free function must be supplied to #{self}.malloc " + + "when it is called with a block" + raise ArgumentError, message + end + + pointer = new(LibC.malloc(size), size, free) + if block_given? + begin + yield(pointer) + ensure + pointer.call_free + end + else + pointer + end + end + + def null? + @ffi_ptr.null? + end + + def to_ptr + @ffi_ptr + end + + def size + defined?(@layout) ? @layout.size : @size + end + + def free + @free + end + + def free=(free) + @free = free + end + + def call_free + return if @free.nil? + return if @freed + if @free == RUBY_FREE + LibC::FREE.call(@ffi_ptr) + else + @free.call(@ffi_ptr) + end + @freed = true + end + + def freed? + @freed + end + + def size=(size) + @size = size + end + + def [](index, length = nil) + if length + ffi_ptr.get_bytes(index, length) + else + ffi_ptr.get_char(index) + end + rescue FFI::NullPointerError + raise DLError.new("NULL pointer dereference") + end + + def to_i + ffi_ptr.to_i + end + alias to_int to_i + + # without \0 + def to_s(len = nil) + if len + ffi_ptr.get_string(0, len) + else + ffi_ptr.get_string(0) + end + rescue FFI::NullPointerError + raise DLError.new("NULL pointer access") + end + + def to_str(len = nil) + if len + ffi_ptr.read_string(len) + else + ffi_ptr.read_string(@size) + end + rescue FFI::NullPointerError + raise DLError.new("NULL pointer access") + end + + def to_value + raise NotImplementedError, "to_value isn't supported" + end + + def inspect + "#<#{self.class.name} ptr=#{to_i.to_s(16)} size=#{@size} free=#{@free.inspect}>" + end + + def +(delta) + self.class.new(to_i + delta, @size - delta) + end + + def -(delta) + self.class.new(to_i - delta, @size + delta) + end + + def <=>(other) + return unless other.is_a?(Pointer) + diff = self.to_i - other.to_i + return 0 if diff == 0 + diff > 0 ? 1 : -1 + end + + def eql?(other) + return unless other.is_a?(Pointer) + self.to_i == other.to_i + end + + def ==(other) + eql?(other) + end + + def ptr + Pointer.new(ffi_ptr.get_pointer(0)) + end + + def +@ + ptr + end + + def -@ + ref + end + + def ref + cptr = Pointer.malloc(FFI::Type::POINTER.size) + cptr.ffi_ptr.put_pointer(0, ffi_ptr) + cptr + end + end + + class Handle + RTLD_GLOBAL = FFI::DynamicLibrary::RTLD_GLOBAL + RTLD_LAZY = FFI::DynamicLibrary::RTLD_LAZY + RTLD_NOW = FFI::DynamicLibrary::RTLD_NOW + + def initialize(libname = nil, flags = RTLD_LAZY | RTLD_GLOBAL) + @lib = FFI::DynamicLibrary.open(libname, flags) rescue LoadError + raise DLError.new("Could not open #{libname}") unless @lib + + @open = true + + begin + yield(self) + ensure + self.close + end if block_given? + end + + def close + raise DLError.new("closed handle") unless @open + @open = false + 0 + end + + def self.sym(func) + DEFAULT.sym(func) + end + + def sym(func) + raise TypeError.new("invalid function name") unless func.is_a?(String) + raise DLError.new("closed handle") unless @open + address = @lib.find_function(func) + raise DLError.new("unknown symbol #{func}") if address.nil? || address.null? + address.to_i + end + + def self.sym_defined?(func) + DEFAULT.sym_defined?(func) + end + + def sym_defined?(func) + raise TypeError.new("invalid function name") unless func.is_a?(String) + raise DLError.new("closed handle") unless @open + address = @lib.find_function(func) + !address.nil? && !address.null? + end + + def self.[](func) + self.sym(func) + end + + def [](func) + sym(func) + end + + def enable_close + @enable_close = true + end + + def close_enabled? + @enable_close + end + + def disable_close + @enable_close = false + end + + DEFAULT = new + end + + class Pinned + def initialize(object) + @object = object + end + + def ref + if @object.nil? + raise ClearedReferenceError, "`ref` called on a cleared object" + end + @object + end + + def clear + @object = nil + end + + def cleared? + @object.nil? + end + end + + RUBY_FREE = Fiddle::Pointer::LibC::FREE.address + NULL = Fiddle::Pointer.new(0) + + ALIGN_VOIDP = Fiddle::JRuby::FFITypes[Types::VOIDP].alignment + ALIGN_CHAR = Fiddle::JRuby::FFITypes[Types::CHAR].alignment + ALIGN_SHORT = Fiddle::JRuby::FFITypes[Types::SHORT].alignment + ALIGN_INT = Fiddle::JRuby::FFITypes[Types::INT].alignment + ALIGN_LONG = Fiddle::JRuby::FFITypes[Types::LONG].alignment + ALIGN_LONG_LONG = Fiddle::JRuby::FFITypes[Types::LONG_LONG].alignment + ALIGN_INT8_T = Fiddle::JRuby::FFITypes[Types::INT8_T].alignment + ALIGN_INT16_T = Fiddle::JRuby::FFITypes[Types::INT16_T].alignment + ALIGN_INT32_T = Fiddle::JRuby::FFITypes[Types::INT32_T].alignment + ALIGN_INT64_T = Fiddle::JRuby::FFITypes[Types::INT64_T].alignment + ALIGN_FLOAT = Fiddle::JRuby::FFITypes[Types::FLOAT].alignment + ALIGN_DOUBLE = Fiddle::JRuby::FFITypes[Types::DOUBLE].alignment + ALIGN_BOOL = Fiddle::JRuby::FFITypes[Types::BOOL].alignment + ALIGN_SIZE_T = Fiddle::JRuby::FFITypes[Types::SIZE_T].alignment + ALIGN_SSIZE_T = ALIGN_SIZE_T + ALIGN_PTRDIFF_T = Fiddle::JRuby::FFITypes[Types::PTRDIFF_T].alignment + ALIGN_INTPTR_T = Fiddle::JRuby::FFITypes[Types::INTPTR_T].alignment + ALIGN_UINTPTR_T = Fiddle::JRuby::FFITypes[Types::UINTPTR_T].alignment + + SIZEOF_VOIDP = Fiddle::JRuby::FFITypes[Types::VOIDP].size + SIZEOF_CHAR = Fiddle::JRuby::FFITypes[Types::CHAR].size + SIZEOF_UCHAR = Fiddle::JRuby::FFITypes[Types::UCHAR].size + SIZEOF_SHORT = Fiddle::JRuby::FFITypes[Types::SHORT].size + SIZEOF_USHORT = Fiddle::JRuby::FFITypes[Types::USHORT].size + SIZEOF_INT = Fiddle::JRuby::FFITypes[Types::INT].size + SIZEOF_UINT = Fiddle::JRuby::FFITypes[Types::UINT].size + SIZEOF_LONG = Fiddle::JRuby::FFITypes[Types::LONG].size + SIZEOF_ULONG = Fiddle::JRuby::FFITypes[Types::ULONG].size + SIZEOF_LONG_LONG = Fiddle::JRuby::FFITypes[Types::LONG_LONG].size + SIZEOF_ULONG_LONG = Fiddle::JRuby::FFITypes[Types::ULONG_LONG].size + SIZEOF_INT8_T = Fiddle::JRuby::FFITypes[Types::INT8_T].size + SIZEOF_UINT8_T = Fiddle::JRuby::FFITypes[Types::UINT8_T].size + SIZEOF_INT16_T = Fiddle::JRuby::FFITypes[Types::INT16_T].size + SIZEOF_UINT16_T = Fiddle::JRuby::FFITypes[Types::UINT16_T].size + SIZEOF_INT32_T = Fiddle::JRuby::FFITypes[Types::INT32_T].size + SIZEOF_UINT32_T = Fiddle::JRuby::FFITypes[Types::UINT32_T].size + SIZEOF_INT64_T = Fiddle::JRuby::FFITypes[Types::INT64_T].size + SIZEOF_UINT64_T = Fiddle::JRuby::FFITypes[Types::UINT64_T].size + SIZEOF_FLOAT = Fiddle::JRuby::FFITypes[Types::FLOAT].size + SIZEOF_DOUBLE = Fiddle::JRuby::FFITypes[Types::DOUBLE].size + SIZEOF_BOOL = Fiddle::JRuby::FFITypes[Types::BOOL].size + SIZEOF_SIZE_T = Fiddle::JRuby::FFITypes[Types::SIZE_T].size + SIZEOF_SSIZE_T = SIZEOF_SIZE_T + SIZEOF_PTRDIFF_T = Fiddle::JRuby::FFITypes[Types::PTRDIFF_T].size + SIZEOF_INTPTR_T = Fiddle::JRuby::FFITypes[Types::INTPTR_T].size + SIZEOF_UINTPTR_T = Fiddle::JRuby::FFITypes[Types::UINTPTR_T].size + SIZEOF_CONST_STRING = Fiddle::JRuby::FFITypes[Types::VOIDP].size +end diff --git a/lib/fiddle/struct.rb b/lib/fiddle/struct.rb index 6d05bbd7..e4c2c79a 100644 --- a/lib/fiddle/struct.rb +++ b/lib/fiddle/struct.rb @@ -290,15 +290,28 @@ def CStructEntity.alignment(types) # Allocates a C struct with the +types+ provided. # # See Fiddle::Pointer.malloc for memory management issues. - def CStructEntity.malloc(types, func = nil, size = size(types), &block) + def CStructEntity.malloc(types, func = nil, size = size(types)) + if block_given? and func.nil? + message = "a free function must be supplied to #{self}.malloc " + + "when it is called with a block" + raise ArgumentError, message + end + + pointer = Pointer.malloc(size) + begin + struct = new(pointer, types, func) + rescue + pointer.free = func + pointer.call_free + raise + end if block_given? - super(size, func) do |struct| - struct.set_ctypes types - yield struct + begin + yield(struct) + ensure + struct.call_free end else - struct = super(size, func) - struct.set_ctypes types struct end end @@ -505,6 +518,14 @@ def []=(*args) def to_s() # :nodoc: super(@size) end + + def +(delta) + Pointer.new(to_i + delta, @size - delta) + end + + def -(delta) + Pointer.new(to_i - delta, @size + delta) + end end # A pointer to a C union diff --git a/test/fiddle/helper.rb b/test/fiddle/helper.rb index e470f5a2..eae39a6e 100644 --- a/test/fiddle/helper.rb +++ b/test/fiddle/helper.rb @@ -4,11 +4,19 @@ require 'test/unit' require 'fiddle' +puts("Fiddle::VERSION: #{Fiddle::VERSION}") + # FIXME: this is stolen from DL and needs to be refactored. libc_so = libm_so = nil -case RUBY_PLATFORM +if RUBY_ENGINE == "jruby" + # "jruby ... [x86_64-linux]" -> "x86_64-linux" + ruby_platform = RUBY_DESCRIPTION.split(" ").last[1..-2] +else + ruby_platform = RUBY_PLATFORM +end +case ruby_platform when /cygwin/ libc_so = "cygwin1.dll" libm_so = "cygwin1.dll" @@ -147,6 +155,7 @@ end if !libc_so || !libm_so + require "envutil" ruby = EnvUtil.rubybin # When the ruby binary is 32-bit and the host is 64-bit, # `ldd ruby` outputs "not a dynamic executable" message. diff --git a/test/fiddle/test_c_struct_entry.rb b/test/fiddle/test_c_struct_entry.rb index 45de2efe..01341044 100644 --- a/test/fiddle/test_c_struct_entry.rb +++ b/test/fiddle/test_c_struct_entry.rb @@ -49,6 +49,11 @@ def test_class_size_with_count end def test_set_ctypes + if RUBY_ENGINE == "jruby" and Fiddle::WINDOWS + omit("JRuby's 'l!' pack string is buggy. " + + "See https://github.com/jruby/jruby/issues/8357 for details") + end + CStructEntity.malloc([TYPE_INT, TYPE_LONG], Fiddle::RUBY_FREE) do |struct| struct.assign_names %w[int long] diff --git a/test/fiddle/test_c_union_entity.rb b/test/fiddle/test_c_union_entity.rb index e0a37575..d0149f12 100644 --- a/test/fiddle/test_c_union_entity.rb +++ b/test/fiddle/test_c_union_entity.rb @@ -21,6 +21,11 @@ def test_class_size_with_count end def test_set_ctypes + if RUBY_ENGINE == "jruby" and Fiddle::WINDOWS + omit("JRuby's 'l!' pack string is buggy. " + + "See https://github.com/jruby/jruby/issues/8357 for details") + end + CUnionEntity.malloc([TYPE_INT, TYPE_LONG], Fiddle::RUBY_FREE) do |union| union.assign_names %w[int long] diff --git a/test/fiddle/test_closure.rb b/test/fiddle/test_closure.rb index abb6bdbd..787a9b63 100644 --- a/test/fiddle/test_closure.rb +++ b/test/fiddle/test_closure.rb @@ -8,6 +8,8 @@ module Fiddle class TestClosure < Fiddle::TestCase def teardown super + # We can't use ObjectSpace with JRuby. + return if RUBY_ENGINE == "jruby" # Ensure freeing all closures. # See https://github.com/ruby/fiddle/issues/102#issuecomment-1241763091 . not_freed_closures = [] @@ -31,19 +33,6 @@ def test_argument_errors end end - def test_type_symbol - Closure.create(:int, [:void]) do |closure| - assert_equal([ - TYPE_INT, - [TYPE_VOID], - ], - [ - closure.instance_variable_get(:@ctype), - closure.instance_variable_get(:@args), - ]) - end - end - def test_call closure_class = Class.new(Closure) do def call @@ -69,6 +58,11 @@ def call thing end def test_const_string + if RUBY_ENGINE == "jruby" + omit("Closure with :const_string works but " + + "Function with :const_string doesn't work with JRuby") + end + closure_class = Class.new(Closure) do def call(string) @return_string = "Hello! #{string}" @@ -94,7 +88,12 @@ def call(bool) end def test_free - Closure.create(:int, [:void]) do |closure| + closure_class = Class.new(Closure) do + def call + 10 + end + end + closure_class.create(:int, [:void]) do |closure| assert(!closure.freed?) closure.free assert(closure.freed?) @@ -115,6 +114,10 @@ def test_block_caller end def test_memsize_ruby_dev_42480 + if RUBY_ENGINE == "jruby" + omit("We can't use ObjectSpace with JRuby") + end + require 'objspace' n = 10000 n.times do diff --git a/test/fiddle/test_fiddle.rb b/test/fiddle/test_fiddle.rb index 9bddb056..b247ae15 100644 --- a/test/fiddle/test_fiddle.rb +++ b/test/fiddle/test_fiddle.rb @@ -6,6 +6,10 @@ class TestFiddle < Fiddle::TestCase def test_nil_true_etc + if RUBY_ENGINE == "jruby" + omit("Fiddle::Q* aren't supported with JRuby") + end + assert_equal Fiddle::Qtrue, Fiddle.dlwrap(true) assert_equal Fiddle::Qfalse, Fiddle.dlwrap(false) assert_equal Fiddle::Qnil, Fiddle.dlwrap(nil) diff --git a/test/fiddle/test_func.rb b/test/fiddle/test_func.rb index df79539e..5d69cc5f 100644 --- a/test/fiddle/test_func.rb +++ b/test/fiddle/test_func.rb @@ -26,6 +26,10 @@ def test_sin end def test_string + if RUBY_ENGINE == "jruby" + omit("Function that returns string doesn't work with JRuby") + end + under_gc_stress do f = Function.new(@libc['strcpy'], [TYPE_VOIDP, TYPE_VOIDP], TYPE_VOIDP) buff = +"000" @@ -82,6 +86,8 @@ def call(x, y) assert_equal("1349", buff, bug4929) end ensure + # We can't use ObjectSpace with JRuby. + return if RUBY_ENGINE == "jruby" # Ensure freeing all closures. # See https://github.com/ruby/fiddle/issues/102#issuecomment-1241763091 . not_freed_closures = [] @@ -113,37 +119,36 @@ def test_snprintf :variadic, ], :int) - output_buffer = " " * 1024 - output = Pointer[output_buffer] + Pointer.malloc(1024, Fiddle::RUBY_FREE) do |output| + written = snprintf.call(output, + output.size, + "int: %d, string: %.*s, const string: %s\n", + :int, -29, + :int, 4, + :voidp, "Hello", + :const_string, "World") + assert_equal("int: -29, string: Hell, const string: World\n", + output[0, written]) - written = snprintf.call(output, - output.size, - "int: %d, string: %.*s, const string: %s\n", - :int, -29, - :int, 4, - :voidp, "Hello", - :const_string, "World") - assert_equal("int: -29, string: Hell, const string: World\n", - output_buffer[0, written]) - - string_like_class = Class.new do - def initialize(string) - @string = string - end + string_like_class = Class.new do + def initialize(string) + @string = string + end - def to_str - @string + def to_str + @string + end end + written = snprintf.call(output, + output.size, + "string: %.*s, const string: %s, uint: %u\n", + :int, 2, + :voidp, "Hello", + :const_string, string_like_class.new("World"), + :int, 29) + assert_equal("string: He, const string: World, uint: 29\n", + output[0, written]) end - written = snprintf.call(output, - output.size, - "string: %.*s, const string: %s, uint: %u\n", - :int, 2, - :voidp, "Hello", - :const_string, string_like_class.new("World"), - :int, 29) - assert_equal("string: He, const string: World, uint: 29\n", - output_buffer[0, written]) end def test_rb_memory_view_available_p diff --git a/test/fiddle/test_function.rb b/test/fiddle/test_function.rb index 2bd67c9d..658874bf 100644 --- a/test/fiddle/test_function.rb +++ b/test/fiddle/test_function.rb @@ -16,6 +16,8 @@ def setup end def teardown + # We can't use ObjectSpace with JRuby. + return if RUBY_ENGINE == "jruby" # Ensure freeing all closures. # See https://github.com/ruby/fiddle/issues/102#issuecomment-1241763091 . not_freed_closures = [] @@ -36,6 +38,10 @@ def test_name end def test_need_gvl? + if RUBY_ENGINE == "jruby" + omit("rb_str_dup() doesn't exit in JRuby") + end + libruby = Fiddle.dlopen(nil) rb_str_dup = Function.new(libruby['rb_str_dup'], [:voidp], @@ -103,6 +109,10 @@ def call one end def test_last_error + if RUBY_ENGINE == "jruby" + omit("Fiddle.last_error doesn't work with JRuby") + end + func = Function.new(@libc['strcpy'], [TYPE_VOIDP, TYPE_VOIDP], TYPE_VOIDP) assert_nil Fiddle.last_error @@ -135,6 +145,10 @@ def test_win32_last_socket_error end def test_strcpy + if RUBY_ENGINE == "jruby" + omit("Function that returns string doesn't work with JRuby") + end + f = Function.new(@libc['strcpy'], [TYPE_VOIDP, TYPE_VOIDP], TYPE_VOIDP) buff = +"000" str = f.call(buff, "123") @@ -149,6 +163,10 @@ def call_proc(string_to_copy) end def test_function_as_proc + if RUBY_ENGINE == "jruby" + omit("Function that returns string doesn't work with JRuby") + end + f = Function.new(@libc['strcpy'], [TYPE_VOIDP, TYPE_VOIDP], TYPE_VOIDP) buff, str = call_proc("123", &f) assert_equal("123", buff) @@ -156,6 +174,10 @@ def test_function_as_proc end def test_function_as_method + if RUBY_ENGINE == "jruby" + omit("Function that returns string doesn't work with JRuby") + end + f = Function.new(@libc['strcpy'], [TYPE_VOIDP, TYPE_VOIDP], TYPE_VOIDP) klass = Class.new do define_singleton_method(:strcpy, &f) @@ -194,6 +216,10 @@ def test_nogvl_poll end def test_no_memory_leak + if RUBY_ENGINE == "jruby" + omit("rb_obj_frozen_p() doesn't exist in JRuby") + end + 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, diff --git a/test/fiddle/test_handle.rb b/test/fiddle/test_handle.rb index 042e517e..ebd126c9 100644 --- a/test/fiddle/test_handle.rb +++ b/test/fiddle/test_handle.rb @@ -9,11 +9,19 @@ class TestHandle < TestCase include Fiddle def test_to_i + if RUBY_ENGINE == "jruby" + omit("Fiddle::Handle#to_i is unavailable with JRuby") + end + handle = Fiddle::Handle.new(LIBC_SO) assert_kind_of Integer, handle.to_i end def test_to_ptr + if RUBY_ENGINE == "jruby" + omit("Fiddle::Handle#to_i is unavailable with JRuby") + end + handle = Fiddle::Handle.new(LIBC_SO) ptr = handle.to_ptr assert_equal ptr.to_i, handle.to_i @@ -26,6 +34,10 @@ def test_static_sym_unknown end def test_static_sym + if RUBY_ENGINE == "jruby" + omit("We can't assume static symbols with JRuby") + end + begin # Linux / Darwin / FreeBSD refute_nil Fiddle::Handle.sym('dlopen') @@ -90,6 +102,10 @@ def test_dlopen_returns_handle end def test_initialize_noargs + if RUBY_ENGINE == "jruby" + omit("rb_str_new() doesn't exist in JRuby") + end + handle = Handle.new refute_nil handle['rb_str_new'] end @@ -117,6 +133,10 @@ def test_disable_close end def test_file_name + if RUBY_ENGINE == "jruby" + omit("Fiddle::Handle::NEXT doesn't exist with JRuby") + end + file_name = Handle.new(LIBC_SO).file_name if file_name assert_kind_of String, file_name @@ -135,6 +155,10 @@ def test_file_name end def test_NEXT + if RUBY_ENGINE == "jruby" + omit("Fiddle::Handle::NEXT doesn't exist with JRuby") + end + begin # Linux / Darwin # @@ -173,9 +197,13 @@ def test_NEXT end unless /mswin|mingw/ =~ RUBY_PLATFORM def test_DEFAULT + if Fiddle::WINDOWS + omit("Fiddle::Handle::DEFAULT doesn't have malloc() on Windows") + end + handle = Handle::DEFAULT refute_nil handle['malloc'] - end unless /mswin|mingw/ =~ RUBY_PLATFORM + end def test_dlerror # FreeBSD (at least 7.2 to 7.2) calls nsdispatch(3) when it calls diff --git a/test/fiddle/test_import.rb b/test/fiddle/test_import.rb index 090ace62..26190325 100644 --- a/test/fiddle/test_import.rb +++ b/test/fiddle/test_import.rb @@ -149,11 +149,15 @@ def test_type_constants def test_unsigned_result() d = (2 ** 31) + 1 - r = LIBC.strtoul(d.to_s, 0, 0) + r = LIBC.strtoul(d.to_s, nil, 0) assert_equal(d, r) end def test_io() + if RUBY_ENGINE == "jruby" + omit("BUILD_RUBY_PLATFORM doesn't exist in JRuby") + end + if( RUBY_PLATFORM != BUILD_RUBY_PLATFORM ) || !defined?(LIBC.fprintf) return end @@ -329,11 +333,12 @@ def test_struct_nested_struct_replace_array_element_nil() def test_struct_nested_struct_replace_array_element_hash() LIBC::StructNestedStruct.malloc(Fiddle::RUBY_FREE) do |s| + s.vertices[0] = nil s.vertices[0] = { position: { x: 10, y: 100, - } + }, } assert_equal({ "position" => { @@ -450,7 +455,7 @@ def test_struct() s.buff = "012345\377" assert_equal([0,1,2,3,4], s.num) assert_equal(?a.ord, s.c) - assert_equal([?0.ord,?1.ord,?2.ord,?3.ord,?4.ord,?5.ord,?\377.ord], s.buff) + assert_equal([?0.ord,?1.ord,?2.ord,?3.ord,?4.ord,?5.ord,"\xFF".ord], s.buff) end end @@ -467,6 +472,10 @@ def test_gettimeofday() end def test_strcpy() + if RUBY_ENGINE == "jruby" + omit("Function that returns string doesn't work with JRuby") + end + buff = +"000" str = LIBC.strcpy(buff, "123") assert_equal("123", buff) diff --git a/test/fiddle/test_pointer.rb b/test/fiddle/test_pointer.rb index 30236be7..f17c8338 100644 --- a/test/fiddle/test_pointer.rb +++ b/test/fiddle/test_pointer.rb @@ -11,19 +11,22 @@ def dlwrap arg end def test_can_read_write_memory - # Allocate some memory - address = Fiddle.malloc(Fiddle::SIZEOF_VOIDP) + if RUBY_ENGINE == "jruby" + omit("Fiddle::Pointer.{read,write} don't exist in JRuby") + end - bytes_to_write = Fiddle::SIZEOF_VOIDP.times.to_a.pack("C*") + # Allocate some memory + Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, Fiddle::RUBY_FREE) do |pointer| + address = pointer.to_i + bytes_to_write = Fiddle::SIZEOF_VOIDP.times.to_a.pack("C*") - # Write to the memory - Fiddle::Pointer.write(address, bytes_to_write) + # Write to the memory + Fiddle::Pointer.write(address, bytes_to_write) - # Read the bytes out again - bytes = Fiddle::Pointer.read(address, Fiddle::SIZEOF_VOIDP) - assert_equal bytes_to_write, bytes - ensure - Fiddle.free address + # Read the bytes out again + bytes = Fiddle::Pointer.read(address, Fiddle::SIZEOF_VOIDP) + assert_equal bytes_to_write, bytes + end end def test_cptr_to_int @@ -110,6 +113,10 @@ def test_plus end def test_inspect + if RUBY_ENGINE == "jruby" + omit("Fiddle::Pointer#inspect is incompatible on JRuby") + end + ptr = Pointer.new(0) inspect = ptr.inspect assert_match(/size=#{ptr.size}/, inspect) @@ -125,6 +132,10 @@ def test_to_ptr_string end def test_to_ptr_io + if RUBY_ENGINE == "jruby" + omit("Fiddle::Pointer.to_ptr(IO) isn't supported with JRuby") + end + Pointer.malloc(10, Fiddle::RUBY_FREE) do |buf| File.open(__FILE__, 'r') do |f| ptr = Pointer.to_ptr f @@ -172,6 +183,10 @@ def test_cmp end def test_ref_ptr + if RUBY_ENGINE == "jruby" + omit("Fiddle.dlwrap([]) isn't supported with JRuby") + end + ary = [0,1,2,4,5] addr = Pointer.new(dlwrap(ary)) assert_equal addr.to_i, addr.ref.ptr.to_i @@ -180,6 +195,10 @@ def test_ref_ptr end def test_to_value + if RUBY_ENGINE == "jruby" + omit("Fiddle.dlwrap([]) isn't supported with JRuby") + end + ary = [0,1,2,4,5] addr = Pointer.new(dlwrap(ary)) assert_equal ary, addr.to_value diff --git a/test/run.rb b/test/run.rb index 454058f4..8f2d7ada 100755 --- a/test/run.rb +++ b/test/run.rb @@ -3,14 +3,19 @@ $VERBOSE = true source_dir = "#{__dir__}/.." -$LOAD_PATH.unshift("#{source_dir}/test") -$LOAD_PATH.unshift("#{source_dir}/lib") +if File.exist?("#{source_dir}/lib") + # Test against Fiddle in source directory + $LOAD_PATH.unshift("#{source_dir}/lib") -build_dir = Dir.pwd -if File.exist?("#{build_dir}/fiddle.so") - $LOAD_PATH.unshift(build_dir) + build_dir = Dir.pwd + if File.exist?("#{build_dir}/fiddle.so") + $LOAD_PATH.unshift(build_dir) + end +else + # Test against Fiddle installed as gem + gem "fiddle" end -Dir.glob("#{source_dir}/test/fiddle/test_*.rb") do |test_rb| - require File.expand_path(test_rb) -end +require "test/unit" + +exit Test::Unit::AutoRunner.run(true, "#{source_dir}/test/fiddle")