From 5247d3e736f77ce19bbb3e69cf5186fa5a7084f4 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Sun, 16 Apr 2023 20:40:17 +0200 Subject: Add support for using FFI in Ractor All objects are shareable now when frozen. All objects can be created in a non-main Ractor. Typedefs are a global mutable state and are not accessable from Ractor other than the main Ractor. So all Function, Struct, etc. must be defined in the main Ractor and can then be used in other Ractors. --- ext/ffi_c/AbstractMemory.c | 18 ++++++++++++++- ext/ffi_c/ArrayType.c | 2 +- ext/ffi_c/Buffer.c | 4 ++-- ext/ffi_c/DynamicLibrary.c | 4 ++-- ext/ffi_c/Function.c | 3 ++- ext/ffi_c/FunctionInfo.c | 2 +- ext/ffi_c/LastError.c | 3 +-- ext/ffi_c/MappedType.c | 2 +- ext/ffi_c/MemoryPointer.c | 3 ++- ext/ffi_c/Pointer.c | 5 ++++- ext/ffi_c/Struct.c | 8 +++++-- ext/ffi_c/StructByValue.c | 2 +- ext/ffi_c/StructLayout.c | 4 ++-- ext/ffi_c/Type.c | 4 ++-- ext/ffi_c/Variadic.c | 2 +- ext/ffi_c/compat.h | 11 ++++++++++ ext/ffi_c/ffi.c | 4 ++++ lib/ffi/autopointer.rb | 1 + lib/ffi/compat.rb | 43 ++++++++++++++++++++++++++++++++++++ lib/ffi/ffi.rb | 1 + lib/ffi/struct.rb | 2 +- spec/ffi/function_spec.rb | 24 ++++++++++++++++++++ spec/ffi/memorypointer_spec.rb | 30 +++++++++++++++++++++++++ spec/ffi/pointer_spec.rb | 10 +++++++++ spec/ffi/spec_helper.rb | 1 + spec/ffi/struct_spec.rb | 50 +++++++++++++++++++++++++++++++++++++----- 26 files changed, 216 insertions(+), 27 deletions(-) create mode 100644 lib/ffi/compat.rb diff --git a/ext/ffi_c/AbstractMemory.c b/ext/ffi_c/AbstractMemory.c index 3cf16a7..3b076bb 100644 --- a/ext/ffi_c/AbstractMemory.c +++ b/ext/ffi_c/AbstractMemory.c @@ -70,7 +70,7 @@ const rb_data_type_t rbffi_abstract_memory_data_type = { /* extern */ }, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; static size_t @@ -316,6 +316,7 @@ static VALUE memory_clear(VALUE self) { AbstractMemory* ptr = MEMORY(self); + checkWrite(ptr); memset(ptr->address, 0, ptr->size); return self; } @@ -687,6 +688,20 @@ memory_copy_from(VALUE self, VALUE rbsrc, VALUE rblen) return self; } +/* + * call-seq: + * res.freeze + * + * Freeze the AbstractMemory object and unset the writable flag. + */ +static VALUE +memory_freeze(VALUE self) +{ + AbstractMemory* ptr = MEMORY(self); + ptr->flags &= ~MEM_WR; + return rb_call_super(0, NULL); +} + AbstractMemory* rbffi_AbstractMemory_Cast(VALUE obj, const rb_data_type_t *data_type) { @@ -1102,6 +1117,7 @@ rbffi_AbstractMemory_Init(VALUE moduleFFI) rb_define_method(classMemory, "type_size", memory_type_size, 0); rb_define_method(classMemory, "[]", memory_aref, 1); rb_define_method(classMemory, "__copy_from__", memory_copy_from, 2); + rb_define_method(classMemory, "freeze", memory_freeze, 0 ); id_to_ptr = rb_intern("to_ptr"); id_call = rb_intern("call"); diff --git a/ext/ffi_c/ArrayType.c b/ext/ffi_c/ArrayType.c index 4bd77fd..b1cbcea 100644 --- a/ext/ffi_c/ArrayType.c +++ b/ext/ffi_c/ArrayType.c @@ -50,7 +50,7 @@ const rb_data_type_t rbffi_array_type_data_type = { /* extern */ .parent = &rbffi_type_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; diff --git a/ext/ffi_c/Buffer.c b/ext/ffi_c/Buffer.c index 0bfcc02..78339f3 100644 --- a/ext/ffi_c/Buffer.c +++ b/ext/ffi_c/Buffer.c @@ -67,7 +67,7 @@ static const rb_data_type_t buffer_data_type = { .parent = &rbffi_abstract_memory_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; static const rb_data_type_t allocated_buffer_data_type = { @@ -80,7 +80,7 @@ static const rb_data_type_t allocated_buffer_data_type = { .parent = &buffer_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; diff --git a/ext/ffi_c/DynamicLibrary.c b/ext/ffi_c/DynamicLibrary.c index 9bfe86e..9abafc7 100644 --- a/ext/ffi_c/DynamicLibrary.c +++ b/ext/ffi_c/DynamicLibrary.c @@ -73,7 +73,7 @@ static const rb_data_type_t rbffi_library_data_type = { }, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; static const rb_data_type_t library_symbol_data_type = { @@ -87,7 +87,7 @@ static const rb_data_type_t library_symbol_data_type = { .parent = &rbffi_pointer_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; static VALUE LibraryClass = Qnil, SymbolClass = Qnil; diff --git a/ext/ffi_c/Function.c b/ext/ffi_c/Function.c index 0e68722..a74136a 100644 --- a/ext/ffi_c/Function.c +++ b/ext/ffi_c/Function.c @@ -108,7 +108,7 @@ static const rb_data_type_t function_data_type = { .parent = &rbffi_pointer_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; VALUE rbffi_FunctionClass = Qnil; @@ -462,6 +462,7 @@ function_set_autorelease(VALUE self, VALUE autorelease) { Function* fn; + rb_check_frozen(self); TypedData_Get_Struct(self, Function, &function_data_type, fn); fn->autorelease = RTEST(autorelease); diff --git a/ext/ffi_c/FunctionInfo.c b/ext/ffi_c/FunctionInfo.c index 7f77621..197c681 100644 --- a/ext/ffi_c/FunctionInfo.c +++ b/ext/ffi_c/FunctionInfo.c @@ -67,7 +67,7 @@ const rb_data_type_t rbffi_fntype_data_type = { /* extern */ .parent = &rbffi_type_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; VALUE rbffi_FunctionTypeClass = Qnil; diff --git a/ext/ffi_c/LastError.c b/ext/ffi_c/LastError.c index d5ef132..877a3d3 100644 --- a/ext/ffi_c/LastError.c +++ b/ext/ffi_c/LastError.c @@ -105,7 +105,7 @@ static const rb_data_type_t thread_data_data_type = { }, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; static ID id_thread_data; @@ -173,7 +173,6 @@ get_last_winapi_error(VALUE self) static VALUE set_last_error(VALUE self, VALUE error) { - #ifdef _WIN32 SetLastError(NUM2INT(error)); #else diff --git a/ext/ffi_c/MappedType.c b/ext/ffi_c/MappedType.c index b1532e1..e61af30 100644 --- a/ext/ffi_c/MappedType.c +++ b/ext/ffi_c/MappedType.c @@ -57,7 +57,7 @@ static const rb_data_type_t mapped_type_data_type = { .parent = &rbffi_type_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; diff --git a/ext/ffi_c/MemoryPointer.c b/ext/ffi_c/MemoryPointer.c index 8227183..a60168e 100644 --- a/ext/ffi_c/MemoryPointer.c +++ b/ext/ffi_c/MemoryPointer.c @@ -64,7 +64,7 @@ static const rb_data_type_t memory_pointer_data_type = { .parent = &rbffi_pointer_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; static VALUE @@ -136,6 +136,7 @@ memptr_free(VALUE self) { Pointer* ptr; + rb_check_frozen(self); TypedData_Get_Struct(self, Pointer, &memory_pointer_data_type, ptr); if (ptr->allocated) { diff --git a/ext/ffi_c/Pointer.c b/ext/ffi_c/Pointer.c index a6e8eb7..dae853a 100644 --- a/ext/ffi_c/Pointer.c +++ b/ext/ffi_c/Pointer.c @@ -33,6 +33,7 @@ #include #include "rbffi.h" #include "rbffi_endian.h" +#include "compat.h" #include "AbstractMemory.h" #include "Pointer.h" @@ -57,7 +58,7 @@ const rb_data_type_t rbffi_pointer_data_type = { /* extern */ .parent = &rbffi_abstract_memory_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; VALUE @@ -397,6 +398,7 @@ ptr_free(VALUE self) { Pointer* ptr; + rb_check_frozen(self); TypedData_Get_Struct(self, Pointer, &rbffi_pointer_data_type, ptr); if (ptr->allocated) { @@ -436,6 +438,7 @@ ptr_autorelease(VALUE self, VALUE autorelease) { Pointer* ptr; + rb_check_frozen(self); TypedData_Get_Struct(self, Pointer, &rbffi_pointer_data_type, ptr); ptr->autorelease = autorelease == Qtrue; diff --git a/ext/ffi_c/Struct.c b/ext/ffi_c/Struct.c index b9b6b2c..99b164d 100644 --- a/ext/ffi_c/Struct.c +++ b/ext/ffi_c/Struct.c @@ -82,7 +82,7 @@ const rb_data_type_t rbffi_struct_data_type = { /* extern */ }, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; VALUE rbffi_StructClass = Qnil; @@ -382,6 +382,7 @@ struct_aset(VALUE self, VALUE fieldName, VALUE value) Struct* s; StructField* f; + rb_check_frozen(self); s = struct_validate(self); f = struct_field(s, fieldName); @@ -421,6 +422,7 @@ struct_set_pointer(VALUE self, VALUE pointer) StructLayout* layout; AbstractMemory* memory; + rb_check_frozen(self); if (!rb_obj_is_kind_of(pointer, rbffi_AbstractMemoryClass)) { rb_raise(rb_eTypeError, "wrong argument type %s (expected Pointer or Buffer)", rb_obj_classname(pointer)); @@ -471,6 +473,7 @@ struct_set_layout(VALUE self, VALUE layout) Struct* s; TypedData_Get_Struct(self, Struct, &rbffi_struct_data_type, s); + rb_check_frozen(self); if (!rb_obj_is_kind_of(layout, rbffi_StructLayoutClass)) { rb_raise(rb_eTypeError, "wrong argument type %s (expected %s)", rb_obj_classname(layout), rb_class2name(rbffi_StructLayoutClass)); @@ -544,7 +547,7 @@ static const rb_data_type_t inline_array_data_type = { }, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; static VALUE @@ -684,6 +687,7 @@ inline_array_aset(VALUE self, VALUE rbIndex, VALUE rbValue) { InlineArray* array; + rb_check_frozen(self); TypedData_Get_Struct(self, InlineArray, &inline_array_data_type, array); if (array->op != NULL) { diff --git a/ext/ffi_c/StructByValue.c b/ext/ffi_c/StructByValue.c index 94061f3..df03684 100644 --- a/ext/ffi_c/StructByValue.c +++ b/ext/ffi_c/StructByValue.c @@ -67,7 +67,7 @@ static const rb_data_type_t sbv_type_data_type = { .parent = &rbffi_type_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; static VALUE diff --git a/ext/ffi_c/StructLayout.c b/ext/ffi_c/StructLayout.c index 548ddb5..613394a 100644 --- a/ext/ffi_c/StructLayout.c +++ b/ext/ffi_c/StructLayout.c @@ -77,7 +77,7 @@ const rb_data_type_t rbffi_struct_layout_data_type = { /* extern */ .parent = &rbffi_type_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; const rb_data_type_t rbffi_struct_field_data_type = { /* extern */ @@ -91,7 +91,7 @@ const rb_data_type_t rbffi_struct_field_data_type = { /* extern */ .parent = &rbffi_type_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; static VALUE diff --git a/ext/ffi_c/Type.c b/ext/ffi_c/Type.c index 194c81e..3431681 100644 --- a/ext/ffi_c/Type.c +++ b/ext/ffi_c/Type.c @@ -64,7 +64,7 @@ const rb_data_type_t rbffi_type_data_type = { /* extern */ }, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; static const rb_data_type_t builtin_type_data_type = { @@ -77,7 +77,7 @@ static const rb_data_type_t builtin_type_data_type = { .parent = &rbffi_type_data_type, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; static size_t diff --git a/ext/ffi_c/Variadic.c b/ext/ffi_c/Variadic.c index a255969..2e9d790 100644 --- a/ext/ffi_c/Variadic.c +++ b/ext/ffi_c/Variadic.c @@ -81,7 +81,7 @@ static const rb_data_type_t variadic_data_type = { }, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE }; diff --git a/ext/ffi_c/compat.h b/ext/ffi_c/compat.h index 889a4be..a8fdcbe 100644 --- a/ext/ffi_c/compat.h +++ b/ext/ffi_c/compat.h @@ -79,6 +79,8 @@ # define RB_GC_GUARD(x) (x) #endif + +/* For compatibility with ruby < 2.7 */ #ifdef HAVE_RB_GC_MARK_MOVABLE #define ffi_compact_callback(x) .dcompact = (x), #define ffi_gc_location(x) x = rb_gc_location(x) @@ -88,4 +90,13 @@ #define ffi_gc_location(x) #endif + +/* For compatibility with ruby < 3.0 */ +#ifndef RUBY_TYPED_FROZEN_SHAREABLE +#define FFI_RUBY_TYPED_FROZEN_SHAREABLE 0 +#else +#define FFI_RUBY_TYPED_FROZEN_SHAREABLE RUBY_TYPED_FROZEN_SHAREABLE +#endif + + #endif /* RBFFI_COMPAT_H */ diff --git a/ext/ffi_c/ffi.c b/ext/ffi_c/ffi.c index 22ea3bf..e297f8a 100644 --- a/ext/ffi_c/ffi.c +++ b/ext/ffi_c/ffi.c @@ -60,6 +60,10 @@ static VALUE moduleFFI = Qnil; void Init_ffi_c(void) { + #ifdef HAVE_RB_EXT_RACTOR_SAFE + rb_ext_ractor_safe(1); + #endif + /* * Document-module: FFI * diff --git a/lib/ffi/autopointer.rb b/lib/ffi/autopointer.rb index 6c6333c..bd4c911 100644 --- a/lib/ffi/autopointer.rb +++ b/lib/ffi/autopointer.rb @@ -107,6 +107,7 @@ module FFI # @return [Boolean] +autorelease+ # Set +autorelease+ property. See {Pointer Autorelease section at Pointer}. def autorelease=(autorelease) + raise FrozenError.new("can't modify frozen #{self.class}") if frozen? @releaser.autorelease=(autorelease) end diff --git a/lib/ffi/compat.rb b/lib/ffi/compat.rb new file mode 100644 index 0000000..7569013 --- /dev/null +++ b/lib/ffi/compat.rb @@ -0,0 +1,43 @@ +# +# Copyright (C) 2023-2023 Lars Kanis +# +# This file is part of ruby-ffi. +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Ruby FFI project nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +module FFI + if defined?(Ractor.make_shareable) + # This is for FFI internal use only. + def self.make_shareable(obj) + Ractor.make_shareable(obj) + end + else + def self.make_shareable(obj) + obj.freeze + end + end +end diff --git a/lib/ffi/ffi.rb b/lib/ffi/ffi.rb index dfffa8c..31f25d1 100644 --- a/lib/ffi/ffi.rb +++ b/lib/ffi/ffi.rb @@ -28,6 +28,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +require 'ffi/compat' require 'ffi/platform' require 'ffi/data_converter' require 'ffi/types' diff --git a/lib/ffi/struct.rb b/lib/ffi/struct.rb index 725b9cb..1283d63 100644 --- a/lib/ffi/struct.rb +++ b/lib/ffi/struct.rb @@ -219,7 +219,7 @@ module FFI end builder.size = @size if defined?(@size) && @size > builder.size cspec = builder.build - @layout = cspec unless self == Struct + @layout = FFI.make_shareable(cspec) unless self == Struct @size = cspec.size return cspec end diff --git a/spec/ffi/function_spec.rb b/spec/ffi/function_spec.rb index 7f90ea3..0d89a7d 100644 --- a/spec/ffi/function_spec.rb +++ b/spec/ffi/function_spec.rb @@ -48,6 +48,30 @@ describe FFI::Function do expect(LibTest.testFunctionAdd(10, 10, function_add)).to eq(20) end + def adder(a, b) + a + b + end + + it "should be 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(@conninfo) do |conninfo| + 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 diff --git a/spec/ffi/memorypointer_spec.rb b/spec/ffi/memorypointer_spec.rb index c8d1b42..904fdc9 100644 --- a/spec/ffi/memorypointer_spec.rb +++ b/spec/ffi/memorypointer_spec.rb @@ -28,6 +28,19 @@ describe "MemoryPointer#total" do expect(MemoryPointer.new(1024).total).to eq 1024 end end +describe "MemoryPointer#clear" do + it "should clear the memory" do + ptr = MemoryPointer.new(:long) + ptr.write_long 1234 + expect(ptr.read_long).to eq(1234) + ptr.clear + expect(ptr.read_long).to eq(0) + end + it "should deny changes when frozen" do + ptr = MemoryPointer.new(:long).freeze + expect{ ptr.clear }.to raise_error(RuntimeError, /memory write/) + end +end describe "MemoryPointer#read_array_of_long" do it "foo" do ptr = MemoryPointer.new(:long, 1024) @@ -76,3 +89,20 @@ describe "MemoryPointer return value" do expect(Stdio.fclose(fp)).to eq 0 unless fp.nil? or fp.null? end end +describe "#autorelease" do + it "should be true by default" do + expect(MemoryPointer.new(8).autorelease?).to be true + end + + it "should return false when autorelease=(false)" do + ptr = MemoryPointer.new(8) + ptr.autorelease = false + expect(ptr.autorelease?).to be false + ptr.free + end + + it "should deny changes when frozen" do + ptr = MemoryPointer.new(8).freeze + expect{ ptr.autorelease = false }.to raise_error(FrozenError) + end +end diff --git a/spec/ffi/pointer_spec.rb b/spec/ffi/pointer_spec.rb index c8242c7..7232a5f 100644 --- a/spec/ffi/pointer_spec.rb +++ b/spec/ffi/pointer_spec.rb @@ -90,6 +90,11 @@ describe "Pointer" do expect(PointerTestLib.ptr_ret_pointer(memory, 0).address).to eq(0xdeadbeef) end + it "#write_pointer frozen object" do + memory = FFI::MemoryPointer.new(:pointer).freeze + expect{ memory.write_pointer(PointerTestLib.ptr_from_address(0xdeadbeef)) }.to raise_error(RuntimeError, /memory write/) + end + it "#read_array_of_pointer" do values = [0x12345678, 0xfeedf00d, 0xdeadbeef] memory = FFI::MemoryPointer.new :pointer, values.size @@ -360,6 +365,11 @@ describe "AutoPointer" do ptr.autorelease = false expect(ptr.autorelease?).to be false end + + it "should deny changes when frozen" do + ptr = ptr_class.new(FFI::Pointer.new(0xdeadbeef)).freeze + expect{ ptr.autorelease = false }.to raise_error(FrozenError) + end end describe "#type_size" do diff --git a/spec/ffi/spec_helper.rb b/spec/ffi/spec_helper.rb index e9ced3d..63f8b78 100644 --- a/spec/ffi/spec_helper.rb +++ b/spec/ffi/spec_helper.rb @@ -9,6 +9,7 @@ require 'objspace' RSpec.configure do |c| c.filter_run_excluding :broken => true + c.filter_run_excluding( :ractor ) unless defined?(Ractor) end module TestLibrary diff --git a/spec/ffi/struct_spec.rb b/spec/ffi/struct_spec.rb index 0ce5273..897e083 100644 --- a/spec/ffi/struct_spec.rb +++ b/spec/ffi/struct_spec.rb @@ -410,6 +410,7 @@ module StructSpecsStructTests s.pointer.put_double(0, 1.0) expect(s.pointer.get_double(0)).to eq(1.0) end + module EnumFields extend FFI::Library TestEnum = enum :test_enum, [:c1, 10, :c2, 20, :c3, 30, :c4, 40] @@ -456,13 +457,24 @@ module StructSpecsStructTests end it "Can have CallbackInfo struct field" do + s = CallbackMember::TestStruct.new + add_proc = lambda { |a, b| a + b } + sub_proc = lambda { |a, b| a - b } + s[:add] = add_proc + s[:sub] = sub_proc + expect(CallbackMember.struct_call_add_cb(s, 40, 2)).to eq(42) + expect(CallbackMember.struct_call_sub_cb(s, 44, 2)).to eq(42) + end + + it "Can use CallbackInfo struct field in Ractor", :ractor do + res = Ractor.new do s = CallbackMember::TestStruct.new - add_proc = lambda { |a, b| a+b } - sub_proc = lambda { |a, b| a-b } + add_proc = lambda { |a, b| a + b } s[:add] = add_proc - s[:sub] = sub_proc - expect(CallbackMember.struct_call_add_cb(s, 40, 2)).to eq(42) - expect(CallbackMember.struct_call_sub_cb(s, 44, 2)).to eq(42) + CallbackMember.struct_call_add_cb(s, 40, 2) + end.take + + expect( res ).to eq(42) end it "Can return its members as a list" do @@ -532,6 +544,34 @@ module StructSpecsStructTests expect(a.members).to eq([:a]) expect(b.members).to eq([:a, :b]) end + + it "should be shareable for Ractor", :ractor do + a = Class.new(FFI::Struct) do + layout :a, :char + end.new + a[:a] = -34 + Ractor.make_shareable(a) + + res = Ractor.new(a) do |a2| + a2[:a] + end.take + + expect( res ).to eq(-34) + end + + it "should be usable with Ractor", :ractor do + class TestStructRactor < FFI::Struct + layout :i, :int + end + + res = Ractor.new(@conninfo) do |conninfo| + s = TestStructRactor.new + s[:i] = 0x14 + LibTest.ptr_ret_int32_t(s, 0) + end.take + + expect( res ).to eq(0x14) + end end end -- cgit v1.2.1 From 8f77740e3bae97eae50ac17132b133db50da197b Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Tue, 18 Apr 2023 10:06:44 +0200 Subject: Use a Ractor-local callback dispatcher for calls from non-ruby threads The callback is now routet to the Ractor, that created the FFI::Function . Within this Ractor a new ruby thread is created to execute the Proc, like before. --- ext/ffi_c/Function.c | 212 ++++++++++++++++++++++++++++------------ spec/ffi/async_callback_spec.rb | 23 +++++ 2 files changed, 171 insertions(+), 64 deletions(-) diff --git a/ext/ffi_c/Function.c b/ext/ffi_c/Function.c index a74136a..f88bc93 100644 --- a/ext/ffi_c/Function.c +++ b/ext/ffi_c/Function.c @@ -42,6 +42,10 @@ #include #include +#if HAVE_RB_EXT_RACTOR_SAFE +#include +#endif + #include #if defined(HAVE_NATIVETHREAD) && !defined(_WIN32) #include @@ -65,6 +69,9 @@ #include "MethodHandle.h" #include "Function.h" +#define DEFER_ASYNC_CALLBACK 1 + +struct async_cb_dispatcher; typedef struct Function_ { Pointer base; FunctionType* info; @@ -73,6 +80,9 @@ typedef struct Function_ { Closure* closure; VALUE rbProc; VALUE rbFunctionInfo; +#if defined(DEFER_ASYNC_CALLBACK) + struct async_cb_dispatcher *dispatcher; +#endif } Function; static void function_mark(void *data); @@ -86,9 +96,6 @@ static void* callback_with_gvl(void* data); static VALUE invoke_callback(VALUE data); static VALUE save_callback_exception(VALUE data, VALUE exc); -#define DEFER_ASYNC_CALLBACK 1 - - #if defined(DEFER_ASYNC_CALLBACK) static VALUE async_cb_event(void *); static VALUE async_cb_call(void *); @@ -113,10 +120,6 @@ static const rb_data_type_t function_data_type = { VALUE rbffi_FunctionClass = Qnil; -#if defined(DEFER_ASYNC_CALLBACK) -static VALUE async_cb_thread = Qnil; -#endif - static ID id_call = 0, id_to_native = 0, id_from_native = 0, id_cbtable = 0, id_cb_ref = 0; struct gvl_callback { @@ -126,7 +129,10 @@ struct gvl_callback { bool done; rbffi_frame_t *frame; #if defined(DEFER_ASYNC_CALLBACK) + struct async_cb_dispatcher *dispatcher; struct gvl_callback* next; + + /* Signal when the callback has finished and retval is set */ # ifndef _WIN32 pthread_cond_t async_cond; pthread_mutex_t async_mutex; @@ -138,16 +144,74 @@ struct gvl_callback { #if defined(DEFER_ASYNC_CALLBACK) -static struct gvl_callback* async_cb_list = NULL; +struct async_cb_dispatcher { + /* the Ractor-local dispatcher thread */ + VALUE thread; + + /* single linked list of pending callbacks */ + struct gvl_callback* async_cb_list; + + /* Signal new entries in async_cb_list */ # ifndef _WIN32 - static pthread_mutex_t async_cb_mutex = PTHREAD_MUTEX_INITIALIZER; - static pthread_cond_t async_cb_cond = PTHREAD_COND_INITIALIZER; + pthread_mutex_t async_cb_mutex; + pthread_cond_t async_cb_cond; # else - static HANDLE async_cb_cond; - static CRITICAL_SECTION async_cb_lock; + HANDLE async_cb_cond; + CRITICAL_SECTION async_cb_lock; # endif -#endif +}; +#if HAVE_RB_EXT_RACTOR_SAFE +static void +async_cb_dispatcher_mark(void *ptr) +{ + struct async_cb_dispatcher *ctx = (struct async_cb_dispatcher *)ptr; + rb_gc_mark(ctx->thread); +} + +static void +async_cb_dispatcher_free(void *ptr) +{ + struct async_cb_dispatcher *ctx = (struct async_cb_dispatcher *)ptr; + xfree(ctx); +} + +struct rb_ractor_local_storage_type async_cb_dispatcher_key_type = { + async_cb_dispatcher_mark, + async_cb_dispatcher_free, +}; + +static rb_ractor_local_key_t async_cb_dispatcher_key; + +static struct async_cb_dispatcher * +async_cb_dispatcher_get(void) +{ + struct async_cb_dispatcher *ctx = (struct async_cb_dispatcher *)rb_ractor_local_storage_ptr(async_cb_dispatcher_key); + return ctx; +} + +static void +async_cb_dispatcher_set(struct async_cb_dispatcher *ctx) +{ + rb_ractor_local_storage_ptr_set(async_cb_dispatcher_key, ctx); +} +#else +// for ruby 2.x +static struct async_cb_dispatcher *async_cb_dispatcher = NULL; + +static struct async_cb_dispatcher * +async_cb_dispatcher_get(void) +{ + return async_cb_dispatcher; +} + +static void +async_cb_dispatcher_set(struct async_cb_dispatcher *ctx) +{ + async_cb_dispatcher = ctx; +} +#endif +#endif static VALUE function_allocate(VALUE klass) @@ -328,9 +392,7 @@ static void after_fork_callback(void) { /* Ensure that a new dispatcher thread is started in a forked process */ - async_cb_thread = Qnil; - pthread_mutex_init(&async_cb_mutex, NULL); - pthread_cond_init(&async_cb_cond, NULL); + async_cb_dispatcher_set(NULL); } #endif @@ -360,17 +422,30 @@ function_init(VALUE self, VALUE rbFunctionInfo, VALUE rbProc) } #if defined(DEFER_ASYNC_CALLBACK) - if (async_cb_thread == Qnil) { + { + struct async_cb_dispatcher *ctx = async_cb_dispatcher_get(); + if (ctx == NULL) { + ctx = (struct async_cb_dispatcher*)ALLOC(struct async_cb_dispatcher); + ctx->async_cb_list = NULL; #if !defined(_WIN32) - if( pthread_atfork(NULL, NULL, after_fork_callback) ){ - rb_warn("FFI: unable to register fork callback"); - } + pthread_mutex_init(&ctx->async_cb_mutex, NULL); + pthread_cond_init(&ctx->async_cb_cond, NULL); + if( pthread_atfork(NULL, NULL, after_fork_callback) ){ + rb_warn("FFI: unable to register fork callback"); + } +#else + InitializeCriticalSection(&ctx->async_cb_lock); + ctx->async_cb_cond = CreateEvent(NULL, FALSE, FALSE, NULL); #endif + ctx->thread = rb_thread_create(async_cb_event, ctx); - async_cb_thread = rb_thread_create(async_cb_event, NULL); - /* Name thread, for better debugging */ - rb_funcall(async_cb_thread, rb_intern("name="), 1, rb_str_new2("FFI Callback Dispatcher")); + /* Name thread, for better debugging */ + rb_funcall(ctx->thread, rb_intern("name="), 1, rb_str_new2("FFI Callback Dispatcher")); + + async_cb_dispatcher_set(ctx); + } + fn->dispatcher = ctx; } #endif @@ -505,6 +580,7 @@ function_release(VALUE self) static void callback_invoke(ffi_cif* cif, void* retval, void** parameters, void* user_data) { + Function* fn; struct gvl_callback cb = { 0 }; cb.closure = (Closure *) user_data; @@ -512,6 +588,7 @@ callback_invoke(ffi_cif* cif, void* retval, void** parameters, void* user_data) cb.parameters = parameters; cb.done = false; cb.frame = rbffi_frame_current(); + fn = (Function *) cb.closure->info; if (cb.frame != NULL) cb.frame->exc = Qnil; @@ -524,18 +601,19 @@ callback_invoke(ffi_cif* cif, void* retval, void** parameters, void* user_data) #if defined(DEFER_ASYNC_CALLBACK) && !defined(_WIN32) } else { bool empty = false; + struct async_cb_dispatcher *ctx = fn->dispatcher; pthread_mutex_init(&cb.async_mutex, NULL); pthread_cond_init(&cb.async_cond, NULL); - /* Now signal the async callback thread */ - pthread_mutex_lock(&async_cb_mutex); - empty = async_cb_list == NULL; - cb.next = async_cb_list; - async_cb_list = &cb; + /* Now signal the async callback dispatcher thread */ + pthread_mutex_lock(&ctx->async_cb_mutex); + empty = ctx->async_cb_list == NULL; + cb.next = ctx->async_cb_list; + ctx->async_cb_list = &cb; - pthread_cond_signal(&async_cb_cond); - pthread_mutex_unlock(&async_cb_mutex); + pthread_cond_signal(&ctx->async_cb_cond); + pthread_mutex_unlock(&ctx->async_cb_mutex); /* Wait for the thread executing the ruby callback to signal it is done */ pthread_mutex_lock(&cb.async_mutex); @@ -549,17 +627,18 @@ callback_invoke(ffi_cif* cif, void* retval, void** parameters, void* user_data) #elif defined(DEFER_ASYNC_CALLBACK) && defined(_WIN32) } else { bool empty = false; + struct async_cb_dispatcher *ctx = fn->dispatcher; cb.async_event = CreateEvent(NULL, FALSE, FALSE, NULL); - /* Now signal the async callback thread */ - EnterCriticalSection(&async_cb_lock); - empty = async_cb_list == NULL; - cb.next = async_cb_list; - async_cb_list = &cb; - LeaveCriticalSection(&async_cb_lock); + /* Now signal the async callback dispatcher thread */ + EnterCriticalSection(&ctx->async_cb_lock); + empty = ctx->async_cb_list == NULL; + cb.next = ctx->async_cb_list; + ctx->async_cb_list = &cb; + LeaveCriticalSection(&ctx->async_cb_lock); - SetEvent(async_cb_cond); + SetEvent(ctx->async_cb_cond); /* Wait for the thread executing the ruby callback to signal it is done */ WaitForSingleObject(cb.async_event, INFINITE); @@ -570,6 +649,7 @@ callback_invoke(ffi_cif* cif, void* retval, void** parameters, void* user_data) #if defined(DEFER_ASYNC_CALLBACK) struct async_wait { + struct async_cb_dispatcher *dispatcher; void* cb; bool stop; }; @@ -578,9 +658,10 @@ static void * async_cb_wait(void *); static void async_cb_stop(void *); static VALUE -async_cb_event(void* unused) +async_cb_event(void* ptr) { - struct async_wait w = { 0 }; + struct async_cb_dispatcher *ctx = (struct async_cb_dispatcher *)ptr; + struct async_wait w = { ctx }; w.stop = false; while (!w.stop) { @@ -601,23 +682,24 @@ static void * async_cb_wait(void *data) { struct async_wait* w = (struct async_wait *) data; + struct async_cb_dispatcher *ctx = w->dispatcher; w->cb = NULL; - EnterCriticalSection(&async_cb_lock); + EnterCriticalSection(&ctx->async_cb_lock); - while (!w->stop && async_cb_list == NULL) { - LeaveCriticalSection(&async_cb_lock); - WaitForSingleObject(async_cb_cond, INFINITE); - EnterCriticalSection(&async_cb_lock); + while (!w->stop && ctx->async_cb_list == NULL) { + LeaveCriticalSection(&ctx->async_cb_lock); + WaitForSingleObject(ctx->async_cb_cond, INFINITE); + EnterCriticalSection(&ctx->async_cb_lock); } - if (async_cb_list != NULL) { - w->cb = async_cb_list; - async_cb_list = async_cb_list->next; + if (ctx->async_cb_list != NULL) { + w->cb = ctx->async_cb_list; + ctx->async_cb_list = ctx->async_cb_list->next; } - LeaveCriticalSection(&async_cb_lock); + LeaveCriticalSection(&ctx->async_cb_lock); return NULL; } @@ -626,11 +708,12 @@ static void async_cb_stop(void *data) { struct async_wait* w = (struct async_wait *) data; + struct async_cb_dispatcher *ctx = w->dispatcher; - EnterCriticalSection(&async_cb_lock); + EnterCriticalSection(&ctx->async_cb_lock); w->stop = true; - LeaveCriticalSection(&async_cb_lock); - SetEvent(async_cb_cond); + LeaveCriticalSection(&ctx->async_cb_lock); + SetEvent(ctx->async_cb_cond); } #else @@ -638,21 +721,22 @@ static void * async_cb_wait(void *data) { struct async_wait* w = (struct async_wait *) data; + struct async_cb_dispatcher *ctx = w->dispatcher; w->cb = NULL; - pthread_mutex_lock(&async_cb_mutex); + pthread_mutex_lock(&ctx->async_cb_mutex); - while (!w->stop && async_cb_list == NULL) { - pthread_cond_wait(&async_cb_cond, &async_cb_mutex); + while (!w->stop && ctx->async_cb_list == NULL) { + pthread_cond_wait(&ctx->async_cb_cond, &ctx->async_cb_mutex); } - if (async_cb_list != NULL) { - w->cb = async_cb_list; - async_cb_list = async_cb_list->next; + if (ctx->async_cb_list != NULL) { + w->cb = ctx->async_cb_list; + ctx->async_cb_list = ctx->async_cb_list->next; } - pthread_mutex_unlock(&async_cb_mutex); + pthread_mutex_unlock(&ctx->async_cb_mutex); return NULL; } @@ -661,11 +745,12 @@ static void async_cb_stop(void *data) { struct async_wait* w = (struct async_wait *) data; + struct async_cb_dispatcher *ctx = w->dispatcher; - pthread_mutex_lock(&async_cb_mutex); + pthread_mutex_lock(&ctx->async_cb_mutex); w->stop = true; - pthread_cond_signal(&async_cb_cond); - pthread_mutex_unlock(&async_cb_mutex); + pthread_cond_signal(&ctx->async_cb_cond); + pthread_mutex_unlock(&ctx->async_cb_mutex); } #endif @@ -961,8 +1046,7 @@ rbffi_Function_Init(VALUE moduleFFI) id_cb_ref = rb_intern("@__ffi_callback__"); id_to_native = rb_intern("to_native"); id_from_native = rb_intern("from_native"); -#if defined(_WIN32) - InitializeCriticalSection(&async_cb_lock); - async_cb_cond = CreateEvent(NULL, FALSE, FALSE, NULL); +#if defined(DEFER_ASYNC_CALLBACK) && defined(HAVE_RB_EXT_RACTOR_SAFE) + async_cb_dispatcher_key = rb_ractor_local_storage_ptr_newkey(&async_cb_dispatcher_key_type); #endif } diff --git a/spec/ffi/async_callback_spec.rb b/spec/ffi/async_callback_spec.rb index 51a4886..7bb9f70 100644 --- a/spec/ffi/async_callback_spec.rb +++ b/spec/ffi/async_callback_spec.rb @@ -50,4 +50,27 @@ describe "async callback" do expect(callback_runner_thread.name).to eq("FFI Callback Runner") end + + it "works in Ractor", :ractor do + skip "not yet supported on TruffleRuby" if RUBY_ENGINE == "truffleruby" + + res = Ractor.new do + v = 0xdeadbeef + correct_ractor = false + correct_thread = false + thread = Thread.current + rac = Ractor.current + cb = Proc.new do |i| + v = i + correct_ractor = rac == Ractor.current + correct_thread = thread != Thread.current + end + LibTest.testAsyncCallback(cb, 0x7fffffff) + + [v, correct_ractor, correct_thread] + end.take + + expect(res).to eq([0x7fffffff, true, true]) + end + end -- cgit v1.2.1 From 82f5942f97731d8a46401d2465d007d89092a45c Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Tue, 18 Apr 2023 10:31:18 +0200 Subject: Ensure errno is usable in Ractor No need to share per thread data object accross ractors. --- ext/ffi_c/LastError.c | 2 +- spec/ffi/errno_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ext/ffi_c/LastError.c b/ext/ffi_c/LastError.c index 877a3d3..f4da301 100644 --- a/ext/ffi_c/LastError.c +++ b/ext/ffi_c/LastError.c @@ -105,7 +105,7 @@ static const rb_data_type_t thread_data_data_type = { }, // IMPORTANT: WB_PROTECTED objects must only use the RB_OBJ_WRITE() // macro to update VALUE references, as to trigger write barriers. - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | FFI_RUBY_TYPED_FROZEN_SHAREABLE + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED }; static ID id_thread_data; diff --git a/spec/ffi/errno_spec.rb b/spec/ffi/errno_spec.rb index 4170f92..c0207b8 100644 --- a/spec/ffi/errno_spec.rb +++ b/spec/ffi/errno_spec.rb @@ -34,4 +34,12 @@ describe "FFI.errno" do expect(FFI.errno).to eq(0x12345678) end end + + it "works in Ractor", :ractor do + res = Ractor.new do + LibTest.setLastError(0x12345678) + FFI.errno + end.take + expect(res).to eq(0x12345678) + end end -- cgit v1.2.1 From 1b78458ff28819cb541d7410616d3d2907e30f73 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Sun, 16 Apr 2023 22:10:32 +0200 Subject: Freeze global typedefs and add per Ractor local custom typedefs Ractor-local custom typedefs are used now, since global writable typedefs aren't compatible with Ractor isolation. Now the builtin typedefs are frozen and available for all Ractors. But all custom typedefs are only per Ractor. Actually the global typedefs already were useable in Ractors, if they are resolved in C code. This is because the C code doesn't check the Ractor boundaries. --- ext/ffi_c/Type.c | 36 ++++++++++++++++++++++++++++++++++++ lib/ffi/types.rb | 26 +++++++++++++++++++++----- spec/ffi/rbx/memory_pointer_spec.rb | 14 ++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/ext/ffi_c/Type.c b/ext/ffi_c/Type.c index 3431681..9bf5681 100644 --- a/ext/ffi_c/Type.c +++ b/ext/ffi_c/Type.c @@ -33,6 +33,9 @@ #include #include +#if HAVE_RB_EXT_RACTOR_SAFE +#include +#endif #include #include "rbffi.h" #include "compat.h" @@ -54,6 +57,9 @@ static VALUE classBuiltinType = Qnil; static VALUE moduleNativeType = Qnil; static VALUE typeMap = Qnil; static ID id_type_size = 0, id_size = 0; +#if HAVE_RB_EXT_RACTOR_SAFE +static rb_ractor_local_key_t custom_typedefs_key; +#endif const rb_data_type_t rbffi_type_data_type = { /* extern */ .wrap_struct_name = "FFI::Type", @@ -245,6 +251,25 @@ rbffi_type_size(VALUE type) } } +static VALUE +custom_typedefs(VALUE self) +{ +#if HAVE_RB_EXT_RACTOR_SAFE + VALUE hash = rb_ractor_local_storage_value(custom_typedefs_key); + if (hash == Qnil) { + hash = rb_hash_new(); + rb_ractor_local_storage_value_set(custom_typedefs_key, hash); + } +#else + static VALUE hash = Qundef; + if (hash == Qundef) { + rb_global_variable(&hash); + hash = rb_hash_new(); + } +#endif + return hash; +} + VALUE rbffi_Type_Lookup(VALUE name) { @@ -254,6 +279,12 @@ rbffi_Type_Lookup(VALUE name) * Try looking up directly in the type map */ VALUE nType; + VALUE cust = custom_typedefs(Qnil); + + if ((nType = rb_hash_lookup(cust, name)) != Qnil && rb_obj_is_kind_of(nType, rbffi_TypeClass)) { + return nType; + } + if ((nType = rb_hash_lookup(typeMap, name)) != Qnil && rb_obj_is_kind_of(nType, rbffi_TypeClass)) { return nType; } @@ -286,6 +317,11 @@ rbffi_Type_Init(VALUE moduleFFI) id_type_size = rb_intern("type_size"); id_size = rb_intern("size"); +#if HAVE_RB_EXT_RACTOR_SAFE + custom_typedefs_key = rb_ractor_local_storage_value_newkey(); +#endif + rb_define_module_function(moduleFFI, "custom_typedefs", custom_typedefs, 0); + /* * Document-class: FFI::Type::Builtin * Class for Built-in types. diff --git a/lib/ffi/types.rb b/lib/ffi/types.rb index 90f50c1..da214ee 100644 --- a/lib/ffi/types.rb +++ b/lib/ffi/types.rb @@ -37,8 +37,11 @@ module FFI # @param [Symbol] add new type definition's name to add # @return [Type] # Add a definition type to type definitions. + # + # The type definition is local per Ractor. def self.typedef(old, add) - TypeDefs[add] = self.find_type(old) + tm = custom_typedefs + tm[add] = self.find_type(old) end # (see FFI.typedef) @@ -46,6 +49,14 @@ module FFI typedef old, add end + class << self + private def __typedef(old, add) + TypeDefs[add] = self.find_type(old) + end + + private :custom_typedefs + end + # @param [Type, DataConverter, Symbol] name # @param [Hash] type_map if nil, {FFI::TypeDefs} is used @@ -57,9 +68,12 @@ module FFI if name.is_a?(Type) name - elsif type_map && type_map.has_key?(name) + elsif type_map&.has_key?(name) type_map[name] + elsif (tm=custom_typedefs).has_key?(name) + tm[name] + elsif TypeDefs.has_key?(name) TypeDefs[name] @@ -168,7 +182,7 @@ module FFI end end - typedef(StrPtrConverter, :strptr) + __typedef(StrPtrConverter, :strptr) # @param type +type+ is an instance of class accepted by {FFI.find_type} # @return [Numeric] @@ -184,11 +198,13 @@ module FFI f.each_line { |line| if line.index(prefix) == 0 new_type, orig_type = line.chomp.slice(prefix.length..-1).split(/\s*=\s*/) - typedef(orig_type.to_sym, new_type.to_sym) + __typedef(orig_type.to_sym, new_type.to_sym) end } end - typedef :pointer, :caddr_t + __typedef :pointer, :caddr_t rescue Errno::ENOENT end + + FFI.make_shareable(TypeDefs) end diff --git a/spec/ffi/rbx/memory_pointer_spec.rb b/spec/ffi/rbx/memory_pointer_spec.rb index 2db89a9..83d77c3 100644 --- a/spec/ffi/rbx/memory_pointer_spec.rb +++ b/spec/ffi/rbx/memory_pointer_spec.rb @@ -104,6 +104,20 @@ describe "MemoryPointer" do expect(m.read FFI::Type::BOOL).to eq(false) end + it "allows definition of a custom typedef" do + FFI.typedef :uint32, :fubar_t + expect(FFI.find_type(:fubar_t)).to eq(FFI::Type::Builtin::UINT32) + end + + it "allows overwriting of a default typedef" do + begin + FFI.typedef :uint32, :char + expect(FFI.find_type(:char)).to eq(FFI::Type::Builtin::UINT32) + ensure + FFI.typedef FFI::Type::Builtin::CHAR, :char + end + end + it "allows writing a custom typedef" do FFI.typedef :uint, :fubar_t FFI.typedef :size_t, :fubar2_t -- cgit v1.2.1 From 1689cc0fa2eba174ec66bffa5d8f1656760c6702 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Tue, 18 Apr 2023 15:06:15 +0200 Subject: Fix compat to JRuby and TuffleRuby regarding Ractor --- lib/ffi/types.rb | 11 ++++++++++- spec/ffi/memorypointer_spec.rb | 4 ++++ spec/ffi/pointer_spec.rb | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/ffi/types.rb b/lib/ffi/types.rb index da214ee..8f5d897 100644 --- a/lib/ffi/types.rb +++ b/lib/ffi/types.rb @@ -33,6 +33,15 @@ # see {file:README} module FFI + unless defined?(self.custom_typedefs) + # Truffleruby and JRuby don't support Ractor so far. + # So they don't need separation between builtin and custom types. + def self.custom_typedefs + TypeDefs + end + writable_typemap = true + end + # @param [Type, DataConverter, Symbol] old type definition used by {FFI.find_type} # @param [Symbol] add new type definition's name to add # @return [Type] @@ -206,5 +215,5 @@ module FFI rescue Errno::ENOENT end - FFI.make_shareable(TypeDefs) + FFI.make_shareable(TypeDefs) unless writable_typemap end diff --git a/spec/ffi/memorypointer_spec.rb b/spec/ffi/memorypointer_spec.rb index 904fdc9..f29e2cb 100644 --- a/spec/ffi/memorypointer_spec.rb +++ b/spec/ffi/memorypointer_spec.rb @@ -37,6 +37,8 @@ describe "MemoryPointer#clear" do expect(ptr.read_long).to eq(0) end it "should deny changes when frozen" do + skip "not yet supported on TruffleRuby" if RUBY_ENGINE == "truffleruby" + skip "not yet supported on JRuby" if RUBY_ENGINE == "jruby" ptr = MemoryPointer.new(:long).freeze expect{ ptr.clear }.to raise_error(RuntimeError, /memory write/) end @@ -102,6 +104,8 @@ describe "#autorelease" do end it "should deny changes when frozen" do + skip "not yet supported on TruffleRuby" if RUBY_ENGINE == "truffleruby" + skip "not yet supported on JRuby" if RUBY_ENGINE == "jruby" ptr = MemoryPointer.new(8).freeze expect{ ptr.autorelease = false }.to raise_error(FrozenError) end diff --git a/spec/ffi/pointer_spec.rb b/spec/ffi/pointer_spec.rb index 7232a5f..880ffd3 100644 --- a/spec/ffi/pointer_spec.rb +++ b/spec/ffi/pointer_spec.rb @@ -91,6 +91,8 @@ describe "Pointer" do end it "#write_pointer frozen object" do + skip "not yet supported on TruffleRuby" if RUBY_ENGINE == "truffleruby" + skip "not yet supported on JRuby" if RUBY_ENGINE == "jruby" memory = FFI::MemoryPointer.new(:pointer).freeze expect{ memory.write_pointer(PointerTestLib.ptr_from_address(0xdeadbeef)) }.to raise_error(RuntimeError, /memory write/) end -- cgit v1.2.1 From bc48722cee0d5dc9982aed633981b8a8b4885880 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Tue, 18 Apr 2023 16:23:32 +0200 Subject: Freeze constant in StructLayoutBuilder since there's no need to modify it later on --- lib/ffi/struct_layout_builder.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ffi/struct_layout_builder.rb b/lib/ffi/struct_layout_builder.rb index 4d6a464..5488033 100644 --- a/lib/ffi/struct_layout_builder.rb +++ b/lib/ffi/struct_layout_builder.rb @@ -97,7 +97,7 @@ module FFI # List of number types - NUMBER_TYPES = [ + NUMBER_TYPES = FFI.make_shareable([ Type::INT8, Type::UINT8, Type::INT16, @@ -112,7 +112,7 @@ module FFI Type::FLOAT64, Type::LONGDOUBLE, Type::BOOL, - ] + ]) # @param [String, Symbol] name name of the field # @param [Array, DataConverter, Struct, StructLayout::Field, Symbol, Type] type type of the field -- cgit v1.2.1 From 88fa99adc880b2ac82d0e963c065c7add65e2137 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Tue, 18 Apr 2023 16:25:32 +0200 Subject: Skip struct specs on Ruby-3.0 It is incompatible to our usage of instance variables by the Struct class. --- spec/ffi/struct_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/ffi/struct_spec.rb b/spec/ffi/struct_spec.rb index 897e083..0429dd5 100644 --- a/spec/ffi/struct_spec.rb +++ b/spec/ffi/struct_spec.rb @@ -467,6 +467,8 @@ module StructSpecsStructTests end it "Can use CallbackInfo struct field in Ractor", :ractor do + skip "sharing structs accross Ractors requires Ruby-3.1" if RUBY_VERSION < "3.1" + res = Ractor.new do s = CallbackMember::TestStruct.new add_proc = lambda { |a, b| a + b } @@ -560,11 +562,13 @@ module StructSpecsStructTests end it "should be usable with Ractor", :ractor do + skip "Using structs in Ractor requires Ruby-3.1" if RUBY_VERSION < "3.1" + class TestStructRactor < FFI::Struct layout :i, :int end - res = Ractor.new(@conninfo) do |conninfo| + res = Ractor.new do s = TestStructRactor.new s[:i] = 0x14 LibTest.ptr_ret_int32_t(s, 0) -- cgit v1.2.1 From 80726b2217eb43a094101377c4273b7b2fdf4833 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Thu, 20 Apr 2023 10:26:48 +0200 Subject: Add a spec file for FFI::DynamicLibrary .. and test Ractor compatibility there. --- spec/ffi/dynamic_library_spec.rb | 56 ++++++++++++++++++++++++++++++++++++++++ spec/ffi/fixtures/compile.rb | 2 +- spec/ffi/library_spec.rb | 17 ------------ 3 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 spec/ffi/dynamic_library_spec.rb diff --git a/spec/ffi/dynamic_library_spec.rb b/spec/ffi/dynamic_library_spec.rb new file mode 100644 index 0000000..a6ea2a9 --- /dev/null +++ b/spec/ffi/dynamic_library_spec.rb @@ -0,0 +1,56 @@ +# +# 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::DynamicLibrary do + it "should be shareable for Ractor", :ractor do + libtest = FFI::DynamicLibrary.open(TestLibrary::PATH, + FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_GLOBAL) + Ractor.make_shareable(libtest) + + res = Ractor.new(libtest) do |libtest2| + libtest2.find_symbol("testClosureVrV").address + end.take + + expect( res ).to be > 0 + end + + it "load a library in a Ractor", :ractor do + res = Ractor.new do + libtest = FFI::DynamicLibrary.open(TestLibrary::PATH, + FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_GLOBAL) + libtest.find_symbol("testClosureVrV") + end.take + + expect(res.address).to be > 0 + end + + it "has a memsize function", skip: RUBY_ENGINE != "ruby" do + base_size = ObjectSpace.memsize_of(Object.new) + + libtest = FFI::DynamicLibrary.open(TestLibrary::PATH, + FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_GLOBAL) + size = ObjectSpace.memsize_of(libtest) + expect(size).to be > base_size + end + + describe Symbol do + before do + @libtest = FFI::DynamicLibrary.open( + TestLibrary::PATH, + FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_GLOBAL, + ) + end + + it "has a memsize function", skip: RUBY_ENGINE != "ruby" do + base_size = ObjectSpace.memsize_of(Object.new) + + symbol = @libtest.find_symbol("gvar_gstruct_set") + size = ObjectSpace.memsize_of(symbol) + expect(size).to be > base_size + end + end +end diff --git a/spec/ffi/fixtures/compile.rb b/spec/ffi/fixtures/compile.rb index 58ee561..2be97cb 100644 --- a/spec/ffi/fixtures/compile.rb +++ b/spec/ffi/fixtures/compile.rb @@ -69,5 +69,5 @@ module TestLibrary lib end - PATH = compile_library(".", "libtest.#{FFI::Platform::LIBSUFFIX}") + PATH = FFI.make_shareable(compile_library(".", "libtest.#{FFI::Platform::LIBSUFFIX}")) end diff --git a/spec/ffi/library_spec.rb b/spec/ffi/library_spec.rb index a17d673..5b806bb 100644 --- a/spec/ffi/library_spec.rb +++ b/spec/ffi/library_spec.rb @@ -322,21 +322,4 @@ describe "Library" do expect(val[:data]).to eq(i) end end - - describe "Symbol" do - before do - @libtest = FFI::DynamicLibrary.open( - TestLibrary::PATH, - FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_GLOBAL, - ) - end - - it "has a memsize function", skip: RUBY_ENGINE != "ruby" do - base_size = ObjectSpace.memsize_of(Object.new) - - symbol = @libtest.find_symbol("gvar_gstruct_set") - size = ObjectSpace.memsize_of(symbol) - expect(size).to be > base_size - end - end end -- cgit v1.2.1 From ad1a2e0cd5970e5d1618782a6ac2d5328811370d Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Thu, 20 Apr 2023 11:02:45 +0200 Subject: Ensure some relevant constants are available in Ractor --- lib/ffi/dynamic_library.rb | 2 +- lib/ffi/library.rb | 2 +- lib/ffi/platform.rb | 28 +++++++++++++++------------- spec/ffi/library_spec.rb | 13 +++++++++++++ spec/ffi/platform_spec.rb | 13 +++++++++++++ 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/lib/ffi/dynamic_library.rb b/lib/ffi/dynamic_library.rb index a5469c4..b415033 100644 --- a/lib/ffi/dynamic_library.rb +++ b/lib/ffi/dynamic_library.rb @@ -35,7 +35,7 @@ module FFI SEARCH_PATH << '/opt/homebrew/lib' end - SEARCH_PATH_MESSAGE = "Searched in , #{SEARCH_PATH.join(', ')}" + SEARCH_PATH_MESSAGE = "Searched in , #{SEARCH_PATH.join(', ')}".freeze def self.load_library(name, flags) if name == FFI::CURRENT_PROCESS diff --git a/lib/ffi/library.rb b/lib/ffi/library.rb index 92d2143..21dce60 100644 --- a/lib/ffi/library.rb +++ b/lib/ffi/library.rb @@ -31,7 +31,7 @@ require 'ffi/dynamic_library' module FFI - CURRENT_PROCESS = USE_THIS_PROCESS_AS_LIBRARY = Object.new + CURRENT_PROCESS = USE_THIS_PROCESS_AS_LIBRARY = FFI.make_shareable(Object.new) # @param [#to_s] lib library name # @return [String] library name formatted for current platform diff --git a/lib/ffi/platform.rb b/lib/ffi/platform.rb index bf01a27..5ac4dd7 100644 --- a/lib/ffi/platform.rb +++ b/lib/ffi/platform.rb @@ -29,13 +29,15 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.# require 'rbconfig' +require_relative 'compat' + module FFI class PlatformError < LoadError; end # This module defines different constants and class methods to play with # various platforms. module Platform - OS = case RbConfig::CONFIG['host_os'].downcase + OS = FFI.make_shareable(case RbConfig::CONFIG['host_os'].downcase when /linux/ "linux" when /darwin/ @@ -54,13 +56,13 @@ module FFI "windows" else RbConfig::CONFIG['host_os'].downcase - end + end) OSVERSION = RbConfig::CONFIG['host_os'].gsub(/[^\d]/, '').to_i - CPU = RbConfig::CONFIG['host_cpu'] + CPU = FFI.make_shareable(RbConfig::CONFIG['host_cpu']) - ARCH = case CPU.downcase + ARCH = FFI.make_shareable(case CPU.downcase when /amd64|x86_64|x64/ "x86_64" when /i\d86|x86|i86pc/ @@ -81,7 +83,7 @@ module FFI end else RbConfig::CONFIG['host_cpu'] - end + end) private # @param [String) os @@ -105,21 +107,21 @@ module FFI # Add the version for known ABI breaks name_version = "12" if IS_FREEBSD && OSVERSION >= 12 # 64-bit inodes - NAME = "#{ARCH}-#{OS}#{name_version}" - CONF_DIR = File.join(File.dirname(__FILE__), 'platform', NAME) + NAME = FFI.make_shareable("#{ARCH}-#{OS}#{name_version}") + CONF_DIR = FFI.make_shareable(File.join(File.dirname(__FILE__), 'platform', NAME)) public - LIBPREFIX = case OS + LIBPREFIX = FFI.make_shareable(case OS when /windows|msys/ '' when /cygwin/ 'cyg' else 'lib' - end + end) - LIBSUFFIX = case OS + LIBSUFFIX = FFI.make_shareable(case OS when /darwin/ 'dylib' when /linux|bsd|solaris/ @@ -129,9 +131,9 @@ module FFI else # Punt and just assume a sane unix (i.e. anything but AIX) 'so' - end + end) - LIBC = if IS_WINDOWS + LIBC = FFI.make_shareable(if IS_WINDOWS crtname = RbConfig::CONFIG["RUBY_SO_NAME"][/msvc\w+/] || 'ucrtbase' "#{crtname}.dll" elsif IS_GNU @@ -143,7 +145,7 @@ module FFI "msys-2.0.dll" else "#{LIBPREFIX}c.#{LIBSUFFIX}" - end + end) LITTLE_ENDIAN = 1234 unless defined?(LITTLE_ENDIAN) BIG_ENDIAN = 4321 unless defined?(BIG_ENDIAN) diff --git a/spec/ffi/library_spec.rb b/spec/ffi/library_spec.rb index 5b806bb..f223d26 100644 --- a/spec/ffi/library_spec.rb +++ b/spec/ffi/library_spec.rb @@ -322,4 +322,17 @@ describe "Library" do expect(val[:data]).to eq(i) end end + + it "should have shareable constants for Ractor", :ractor do + res = Ractor.new do + [ + FFI::Library::LIBC, + FFI::Library::CURRENT_PROCESS, + FFI::CURRENT_PROCESS, + FFI::USE_THIS_PROCESS_AS_LIBRARY, + ] + end.take + + expect( res.size ).to be > 0 + end end diff --git a/spec/ffi/platform_spec.rb b/spec/ffi/platform_spec.rb index ad23621..8890b07 100644 --- a/spec/ffi/platform_spec.rb +++ b/spec/ffi/platform_spec.rb @@ -134,4 +134,17 @@ describe "FFI::Platform.unix?" do expect(FFI::Platform::BYTE_ORDER).to eq(order) end end + + it "should have shareable constants for Ractor", :ractor do + res = Ractor.new do + [ + FFI::Platform::OS, + FFI::Platform::CPU, + FFI::Platform::ARCH, + FFI::Platform::OS, + ] + end.take + + expect( res.size ).to be > 0 + end end -- cgit v1.2.1 From e987ab50366a4b08617a20568eabdaa1fb761317 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Fri, 21 Apr 2023 08:34:07 +0200 Subject: Add the possibility to query attached funtions and variables --- ext/ffi_c/Function.c | 21 +++++++++++++++++--- ext/ffi_c/MappedType.c | 10 ++++++++++ ext/ffi_c/Variadic.c | 9 +++++++++ lib/ffi/ffi.rb | 1 + lib/ffi/function.rb | 44 ++++++++++++++++++++++++++++++++++++++++++ lib/ffi/library.rb | 27 +++++++++++++++++++++----- lib/ffi/variadic.rb | 19 +++++++++++------- spec/ffi/function_spec.rb | 2 +- spec/ffi/library_spec.rb | 15 ++++++++++++++ spec/ffi/struct_by_ref_spec.rb | 5 +++++ spec/ffi/variadic_spec.rb | 29 ++++++++++++++++++++++++++++ 11 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 lib/ffi/function.rb diff --git a/ext/ffi_c/Function.c b/ext/ffi_c/Function.c index f88bc93..40d613e 100644 --- a/ext/ffi_c/Function.c +++ b/ext/ffi_c/Function.c @@ -492,8 +492,8 @@ static VALUE function_attach(VALUE self, VALUE module, VALUE name) { Function* fn; - char var[1024]; + StringValue(name); TypedData_Get_Struct(self, Function, &function_data_type, fn); if (fn->info->parameterCount == -1) { @@ -513,8 +513,12 @@ function_attach(VALUE self, VALUE module, VALUE name) /* * Stash the Function in a module variable so it does not get garbage collected */ - snprintf(var, sizeof(var), "@@%s", StringValueCStr(name)); - rb_cv_set(module, var, self); + VALUE funcs = rb_iv_get(module, "@ffi_functions"); + if (RB_NIL_P(funcs)) { + funcs = rb_hash_new(); + rb_iv_set(module, "@ffi_functions", funcs); + } + rb_hash_aset(funcs, name, self); rb_define_singleton_method(module, StringValueCStr(name), rbffi_MethodHandle_CodeAddress(fn->methodHandle), -1); @@ -555,6 +559,16 @@ function_autorelease_p(VALUE self) return fn->autorelease ? Qtrue : Qfalse; } +static VALUE +function_type(VALUE self) +{ + Function* fn; + + TypedData_Get_Struct(self, Function, &function_data_type, fn); + + return fn->rbFunctionInfo; +} + /* * call-seq: free * @return [self] @@ -1027,6 +1041,7 @@ rbffi_Function_Init(VALUE moduleFFI) rb_define_method(rbffi_FunctionClass, "attach", function_attach, 2); rb_define_method(rbffi_FunctionClass, "free", function_release, 0); rb_define_method(rbffi_FunctionClass, "autorelease=", function_set_autorelease, 1); + rb_define_private_method(rbffi_FunctionClass, "type", function_type, 0); /* * call-seq: autorelease * @return [Boolean] diff --git a/ext/ffi_c/MappedType.c b/ext/ffi_c/MappedType.c index e61af30..20b14e9 100644 --- a/ext/ffi_c/MappedType.c +++ b/ext/ffi_c/MappedType.c @@ -175,6 +175,15 @@ mapped_from_native(int argc, VALUE* argv, VALUE self) return rb_funcall2(m->rbConverter, id_from_native, argc, argv); } +static VALUE +mapped_converter(VALUE self) +{ + MappedType*m = NULL; + TypedData_Get_Struct(self, MappedType, &mapped_type_data_type, m); + + return m->rbConverter; +} + void rbffi_MappedType_Init(VALUE moduleFFI) { @@ -195,5 +204,6 @@ rbffi_MappedType_Init(VALUE moduleFFI) rb_define_method(rbffi_MappedTypeClass, "native_type", mapped_native_type, 0); rb_define_method(rbffi_MappedTypeClass, "to_native", mapped_to_native, -1); rb_define_method(rbffi_MappedTypeClass, "from_native", mapped_from_native, -1); + rb_define_method(rbffi_MappedTypeClass, "converter", mapped_converter, 0); } diff --git a/ext/ffi_c/Variadic.c b/ext/ffi_c/Variadic.c index 2e9d790..fb37194 100644 --- a/ext/ffi_c/Variadic.c +++ b/ext/ffi_c/Variadic.c @@ -318,6 +318,14 @@ variadic_invoke(VALUE self, VALUE parameterTypes, VALUE parameterValues) return rbffi_NativeValue_ToRuby(invoker->returnType, invoker->rbReturnType, retval); } +static VALUE +variadic_result_type(VALUE self) +{ + VariadicInvoker* invoker; + + TypedData_Get_Struct(self, VariadicInvoker, &variadic_data_type, invoker); + return invoker->rbReturnType; +} void rbffi_Variadic_Init(VALUE moduleFFI) @@ -329,5 +337,6 @@ rbffi_Variadic_Init(VALUE moduleFFI) rb_define_method(classVariadicInvoker, "initialize", variadic_initialize, 4); rb_define_method(classVariadicInvoker, "invoke", variadic_invoke, 2); + rb_define_method(classVariadicInvoker, "result_type", variadic_result_type, 0); } diff --git a/lib/ffi/ffi.rb b/lib/ffi/ffi.rb index 31f25d1..bc1b2e6 100644 --- a/lib/ffi/ffi.rb +++ b/lib/ffi/ffi.rb @@ -46,3 +46,4 @@ require 'ffi/autopointer' require 'ffi/variadic' require 'ffi/enum' require 'ffi/version' +require 'ffi/function' diff --git a/lib/ffi/function.rb b/lib/ffi/function.rb new file mode 100644 index 0000000..5996d96 --- /dev/null +++ b/lib/ffi/function.rb @@ -0,0 +1,44 @@ +# +# Copyright (C) 2008-2010 JRuby project +# +# This file is part of ruby-ffi. +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the Ruby FFI project nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +module FFI + class Function + # Only MRI allows function type queries + if private_method_defined?(:type) + def result_type + type.result_type + end + + def param_types + type.param_types + end + end + end +end diff --git a/lib/ffi/library.rb b/lib/ffi/library.rb index 21dce60..5656b2d 100644 --- a/lib/ffi/library.rb +++ b/lib/ffi/library.rb @@ -295,9 +295,10 @@ module FFI # If it is a global struct, just attach directly to the pointer s = s = type.new(address) # Assigning twice to suppress unused variable warning self.module_eval <<-code, __FILE__, __LINE__ - @@ffi_gvar_#{mname} = s + @ffi_gvars = {} unless defined?(@ffi_gvars) + @ffi_gvars[#{mname.inspect}] = s def self.#{mname} - @@ffi_gvar_#{mname} + @ffi_gvars[#{mname.inspect}] end code @@ -309,12 +310,13 @@ module FFI # Attach to this module as mname/mname= # self.module_eval <<-code, __FILE__, __LINE__ - @@ffi_gvar_#{mname} = s + @ffi_gvars = {} unless defined?(@ffi_gvars) + @ffi_gvars[#{mname.inspect}] = s def self.#{mname} - @@ffi_gvar_#{mname}[:gvar] + @ffi_gvars[#{mname.inspect}][:gvar] end def self.#{mname}=(value) - @@ffi_gvar_#{mname}[:gvar] = value + @ffi_gvars[#{mname.inspect}][:gvar] = value end code @@ -539,5 +541,20 @@ module FFI end || FFI.find_type(t) end + + def attached_functions + @ffi_functions || {} + end + + def attached_variables + (@ffi_gvars || {}).map do |name, gvar| + [name, gvar.class] + end.to_h + end + + def freeze + super + FFI.make_shareable(@ffi_functions) + end end end diff --git a/lib/ffi/variadic.rb b/lib/ffi/variadic.rb index 743ce7f..42c5549 100644 --- a/lib/ffi/variadic.rb +++ b/lib/ffi/variadic.rb @@ -55,15 +55,20 @@ module FFI params = "*args" call = "call" mod.module_eval <<-code - @@#{mname} = invoker - def self.#{mname}(#{params}) - @@#{mname}.#{call}(#{params}) - end - def #{mname}(#{params}) - @@#{mname}.#{call}(#{params}) - end + @ffi_functions = {} unless defined?(@ffi_functions) + @ffi_functions[#{mname.inspect}] = invoker + + def self.#{mname}(#{params}) + @ffi_functions[#{mname.inspect}].#{call}(#{params}) + end + + define_method(#{mname.inspect}, &method(:#{mname})) code invoker end + + def param_types + [*@fixed, Type::Builtin::VARARGS] + end end end diff --git a/spec/ffi/function_spec.rb b/spec/ffi/function_spec.rb index 0d89a7d..badd240 100644 --- a/spec/ffi/function_spec.rb +++ b/spec/ffi/function_spec.rb @@ -64,7 +64,7 @@ describe FFI::Function do end it "should be usable with Ractor", :ractor do - res = Ractor.new(@conninfo) do |conninfo| + res = Ractor.new do function_add = FFI::Function.new(:int, [:int, :int]) { |a, b| a + b } LibTest.testFunctionAdd(10, 10, function_add) end.take diff --git a/spec/ffi/library_spec.rb b/spec/ffi/library_spec.rb index f223d26..97790e5 100644 --- a/spec/ffi/library_spec.rb +++ b/spec/ffi/library_spec.rb @@ -193,6 +193,19 @@ describe "Library" do end expect(mod.bool_return_true).to be true end + + it "can reveal the function type" do + mod = Module.new do |m| + m.extend FFI::Library + ffi_lib File.expand_path(TestLibrary::PATH) + attach_function :bool_return_true, [ :string ], :bool + end + + fun = mod.attached_functions + expect(fun.keys).to eq(["bool_return_true"]) + expect(fun["bool_return_true"].param_types).to eq([FFI::Type::STRING]) + expect(fun["bool_return_true"].result_type).to eq(FFI::Type::BOOL) + end end def gvar_lib(name, type) @@ -320,6 +333,8 @@ describe "Library" do lib.gvar[:data] = i val = GlobalStruct.new(lib.get) expect(val[:data]).to eq(i) + + expect(lib.attached_variables).to eq({ gvar: GlobalStruct }) end end diff --git a/spec/ffi/struct_by_ref_spec.rb b/spec/ffi/struct_by_ref_spec.rb index 0858423..0f48fbb 100644 --- a/spec/ffi/struct_by_ref_spec.rb +++ b/spec/ffi/struct_by_ref_spec.rb @@ -39,5 +39,10 @@ describe FFI::Struct, ' by_ref' do expect { @api.struct_test(other_class.new) }.to raise_error(TypeError) end + + it "can reveal the mapped type converter" do + param_type = @api.attached_functions["struct_test"].param_types[0] + expect(param_type.converter).to be_a(FFI::StructByReference) + end end diff --git a/spec/ffi/variadic_spec.rb b/spec/ffi/variadic_spec.rb index f379ed4..fdaf31c 100644 --- a/spec/ffi/variadic_spec.rb +++ b/spec/ffi/variadic_spec.rb @@ -23,6 +23,8 @@ describe "Function with variadic arguments" do attach_function :testBlockingClose, [ :pointer ], :void attach_function :testCallbackVrDva, :testClosureVrDva, [ :double, :varargs ], :double attach_function :testCallbackVrILva, :testClosureVrILva, [ :int, :long, :varargs ], :long + + freeze end it "takes enum arguments" do @@ -37,6 +39,12 @@ describe "Function with variadic arguments" do expect(LibTest.pack_varargs2(buf, :c1, "ii", :int, :c3, :int, :c4)).to eq(:c2) end + it "can reveal its return and parameters" do + fun = LibTest.attached_functions["testBlockingWRva"] + expect(fun.param_types).to eq([FFI::Type::POINTER, FFI::Type::CHAR, FFI::Type::VARARGS]) + expect(fun.result_type).to eq(FFI::Type::INT8) + end + it 'can wrap a blocking function with varargs' do handle = LibTest.testBlockingOpen expect(handle).not_to be_null @@ -87,12 +95,33 @@ describe "Function with variadic arguments" do expect(LibTest.testCallbackVrDva(3.0, :cbVrD, pr)).to be_within(0.0000001).of(45.0) end + it "can be called as instance method" do + kl = Class.new do + include LibTest + def call + pr = proc { 42.0 } + testCallbackVrDva(3.0, :cbVrD, pr) + end + end + expect(kl.new.call).to be_within(0.0000001).of(45.0) + end + it "call variadic with several callback arguments" do pr1 = proc { |i| i + 1 } pr2 = proc { |l| l + 2 } expect(LibTest.testCallbackVrILva(5, 6, :cbVrI, pr1, :cbVrL, pr2)).to eq(14) end + it "should be usable with Ractor", :ractor do + res = Ractor.new do + pr = proc { 42.0 } + LibTest.testCallbackVrDva(3.0, :cbVrD, pr) + end.take + + expect(res).to be_within(0.0000001).of(45.0) + end + + module Varargs PACK_VALUES = { 'c' => [ 0x12 ], -- cgit v1.2.1 From 4fc6a8c5ec8a9a720330946af9d1103015c62942 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Wed, 26 Apr 2023 14:00:17 +0200 Subject: Store each FFI::Function in it's own instance variabe in the module to be attached This allows to freeze the FFI::Function immediately, so that it is shareable by Ractor without the need to freeze the module explicit. To make it shareable the typedef hash used for variadic functions is duplicated and made frozen. This creates a small compatibility issue: Only typedefs defined above the variadic function can be used by that function. If a typedef is created after the definition of the variadic function, then this typedef can no longer be used as parameter to that variadic function. Also fix the retrieval of simple (non-struct) global variables per #attached_variables. Closes #975 --- ext/ffi_c/Function.c | 12 +++++------- ext/ffi_c/Variadic.c | 6 +++++- ext/ffi_c/compat.h | 3 +++ lib/ffi/library.rb | 34 +++++++++++++++++----------------- lib/ffi/variadic.rb | 5 ++--- spec/ffi/library_spec.rb | 18 +++++++++++++++++- spec/ffi/struct_by_ref_spec.rb | 1 + spec/ffi/variadic_spec.rb | 2 -- 8 files changed, 50 insertions(+), 31 deletions(-) diff --git a/ext/ffi_c/Function.c b/ext/ffi_c/Function.c index 40d613e..9da6b37 100644 --- a/ext/ffi_c/Function.c +++ b/ext/ffi_c/Function.c @@ -492,6 +492,7 @@ static VALUE function_attach(VALUE self, VALUE module, VALUE name) { Function* fn; + char var[1024]; StringValue(name); TypedData_Get_Struct(self, Function, &function_data_type, fn); @@ -511,14 +512,11 @@ function_attach(VALUE self, VALUE module, VALUE name) } /* - * Stash the Function in a module variable so it does not get garbage collected + * Stash the Function in a module variable so it does not get garbage collected and can be inspected by attached_functions */ - VALUE funcs = rb_iv_get(module, "@ffi_functions"); - if (RB_NIL_P(funcs)) { - funcs = rb_hash_new(); - rb_iv_set(module, "@ffi_functions", funcs); - } - rb_hash_aset(funcs, name, self); + snprintf(var, sizeof(var), "@ffi_function_%s", StringValueCStr(name)); + rb_ractor_make_shareable(self); + rb_iv_set(module, var, self); rb_define_singleton_method(module, StringValueCStr(name), rbffi_MethodHandle_CodeAddress(fn->methodHandle), -1); diff --git a/ext/ffi_c/Variadic.c b/ext/ffi_c/Variadic.c index fb37194..cb5081b 100644 --- a/ext/ffi_c/Variadic.c +++ b/ext/ffi_c/Variadic.c @@ -36,6 +36,9 @@ #include #include #include +#if HAVE_RB_EXT_RACTOR_SAFE +#include +#endif #include #include "rbffi.h" @@ -181,7 +184,8 @@ variadic_initialize(VALUE self, VALUE rbFunction, VALUE rbParameterTypes, VALUE * @fixed and @type_map are used by the parameter mangling ruby code */ rb_iv_set(self, "@fixed", fixed); - rb_iv_set(self, "@type_map", rb_hash_aref(options, ID2SYM(rb_intern("type_map")))); + rb_iv_set(self, "@type_map", rb_obj_dup(rb_hash_aref(options, ID2SYM(rb_intern("type_map"))))); + rb_ractor_make_shareable(self); return retval; } diff --git a/ext/ffi_c/compat.h b/ext/ffi_c/compat.h index a8fdcbe..27a99be 100644 --- a/ext/ffi_c/compat.h +++ b/ext/ffi_c/compat.h @@ -98,5 +98,8 @@ #define FFI_RUBY_TYPED_FROZEN_SHAREABLE RUBY_TYPED_FROZEN_SHAREABLE #endif +#ifndef HAVE_RB_EXT_RACTOR_SAFE +#define rb_ractor_make_shareable(self) rb_obj_freeze(self); +#endif #endif /* RBFFI_COMPAT_H */ diff --git a/lib/ffi/library.rb b/lib/ffi/library.rb index 5656b2d..e681b3e 100644 --- a/lib/ffi/library.rb +++ b/lib/ffi/library.rb @@ -209,7 +209,7 @@ module FFI end raise LoadError unless function - invokers << if arg_types.length > 0 && arg_types[arg_types.length - 1] == FFI::NativeType::VARARGS + invokers << if arg_types[-1] == FFI::NativeType::VARARGS VariadicInvoker.new(function, arg_types, find_type(ret_type), options) else @@ -295,10 +295,9 @@ module FFI # If it is a global struct, just attach directly to the pointer s = s = type.new(address) # Assigning twice to suppress unused variable warning self.module_eval <<-code, __FILE__, __LINE__ - @ffi_gvars = {} unless defined?(@ffi_gvars) - @ffi_gvars[#{mname.inspect}] = s + @ffi_gsvar_#{mname} = s def self.#{mname} - @ffi_gvars[#{mname.inspect}] + @ffi_gsvar_#{mname} end code @@ -310,13 +309,12 @@ module FFI # Attach to this module as mname/mname= # self.module_eval <<-code, __FILE__, __LINE__ - @ffi_gvars = {} unless defined?(@ffi_gvars) - @ffi_gvars[#{mname.inspect}] = s + @ffi_gvar_#{mname} = s def self.#{mname} - @ffi_gvars[#{mname.inspect}][:gvar] + @ffi_gvar_#{mname}[:gvar] end def self.#{mname}=(value) - @ffi_gvars[#{mname.inspect}][:gvar] = value + @ffi_gvar_#{mname}[:gvar] = value end code @@ -543,18 +541,20 @@ module FFI end def attached_functions - @ffi_functions || {} - end - - def attached_variables - (@ffi_gvars || {}).map do |name, gvar| - [name, gvar.class] + instance_variables.grep(/\A@ffi_function_(.*)/) do |m| + [$1, instance_variable_get(m)] end.to_h end - def freeze - super - FFI.make_shareable(@ffi_functions) + def attached_variables + ( + instance_variables.grep(/\A@ffi_gsvar_(.*)/) do |m| + [$1, instance_variable_get(m).class] + end + + instance_variables.grep(/\A@ffi_gvar_(.*)/) do |m| + [$1, instance_variable_get(m).layout[:gvar].type] + end + ).to_h end end end diff --git a/lib/ffi/variadic.rb b/lib/ffi/variadic.rb index 42c5549..800d121 100644 --- a/lib/ffi/variadic.rb +++ b/lib/ffi/variadic.rb @@ -55,11 +55,10 @@ module FFI params = "*args" call = "call" mod.module_eval <<-code - @ffi_functions = {} unless defined?(@ffi_functions) - @ffi_functions[#{mname.inspect}] = invoker + @ffi_function_#{mname} = invoker def self.#{mname}(#{params}) - @ffi_functions[#{mname.inspect}].#{call}(#{params}) + @ffi_function_#{mname}.#{call}(#{params}) end define_method(#{mname.inspect}, &method(:#{mname})) diff --git a/spec/ffi/library_spec.rb b/spec/ffi/library_spec.rb index 97790e5..c9542b9 100644 --- a/spec/ffi/library_spec.rb +++ b/spec/ffi/library_spec.rb @@ -333,9 +333,25 @@ describe "Library" do lib.gvar[:data] = i val = GlobalStruct.new(lib.get) expect(val[:data]).to eq(i) + end + end - expect(lib.attached_variables).to eq({ gvar: GlobalStruct }) + it "can reveal its attached global struct based variables" do + lib = Module.new do |m| + m.extend FFI::Library + ffi_lib TestLibrary::PATH + attach_variable :gvari, "gvar_gstruct", GlobalStruct + end + expect(lib.attached_variables).to eq({ "gvari" => GlobalStruct }) + end + + it "can reveal its attached global variables" do + lib = Module.new do |m| + m.extend FFI::Library + ffi_lib TestLibrary::PATH + attach_variable :gvaro, "gvar_u32", :uint32 end + expect(lib.attached_variables).to eq({ "gvaro" => FFI::Type::UINT32 }) end it "should have shareable constants for Ractor", :ractor do diff --git a/spec/ffi/struct_by_ref_spec.rb b/spec/ffi/struct_by_ref_spec.rb index 0f48fbb..0c01137 100644 --- a/spec/ffi/struct_by_ref_spec.rb +++ b/spec/ffi/struct_by_ref_spec.rb @@ -42,6 +42,7 @@ describe FFI::Struct, ' by_ref' do it "can reveal the mapped type converter" do param_type = @api.attached_functions["struct_test"].param_types[0] + expect(param_type).to be_a(FFI::Type::Mapped) expect(param_type.converter).to be_a(FFI::StructByReference) end end diff --git a/spec/ffi/variadic_spec.rb b/spec/ffi/variadic_spec.rb index fdaf31c..283878b 100644 --- a/spec/ffi/variadic_spec.rb +++ b/spec/ffi/variadic_spec.rb @@ -23,8 +23,6 @@ describe "Function with variadic arguments" do attach_function :testBlockingClose, [ :pointer ], :void attach_function :testCallbackVrDva, :testClosureVrDva, [ :double, :varargs ], :double attach_function :testCallbackVrILva, :testClosureVrILva, [ :int, :long, :varargs ], :long - - freeze end it "takes enum arguments" do -- cgit v1.2.1 From 0093113f07d1878d6f70480cdc1bf95c09fb7c2a Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Wed, 26 Apr 2023 14:50:29 +0200 Subject: Rename result_type to return_type The term "return_type" is already used in Truffleruby and sounds better to me. On the other hand the term "result_type" was only used in C-ffi as FunctionInfo#result_type, which is an internal class only. --- ext/ffi_c/FunctionInfo.c | 6 +++--- ext/ffi_c/Variadic.c | 4 ++-- lib/ffi/function.rb | 4 ++-- spec/ffi/function_type_spec.rb | 2 +- spec/ffi/library_spec.rb | 2 +- spec/ffi/variadic_spec.rb | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ext/ffi_c/FunctionInfo.c b/ext/ffi_c/FunctionInfo.c index 197c681..7495215 100644 --- a/ext/ffi_c/FunctionInfo.c +++ b/ext/ffi_c/FunctionInfo.c @@ -255,12 +255,12 @@ fntype_initialize(int argc, VALUE* argv, VALUE self) } /* - * call-seq: result_type + * call-seq: return_type * @return [Type] * Get the return type of the function type */ static VALUE -fntype_result_type(VALUE self) +fntype_return_type(VALUE self) { FunctionType* ft; @@ -311,7 +311,7 @@ rbffi_FunctionInfo_Init(VALUE moduleFFI) rb_define_alloc_func(rbffi_FunctionTypeClass, fntype_allocate); rb_define_method(rbffi_FunctionTypeClass, "initialize", fntype_initialize, -1); - rb_define_method(rbffi_FunctionTypeClass, "result_type", fntype_result_type, 0); + rb_define_method(rbffi_FunctionTypeClass, "return_type", fntype_return_type, 0); rb_define_method(rbffi_FunctionTypeClass, "param_types", fntype_param_types, 0); } diff --git a/ext/ffi_c/Variadic.c b/ext/ffi_c/Variadic.c index cb5081b..0f321d9 100644 --- a/ext/ffi_c/Variadic.c +++ b/ext/ffi_c/Variadic.c @@ -323,7 +323,7 @@ variadic_invoke(VALUE self, VALUE parameterTypes, VALUE parameterValues) } static VALUE -variadic_result_type(VALUE self) +variadic_return_type(VALUE self) { VariadicInvoker* invoker; @@ -341,6 +341,6 @@ rbffi_Variadic_Init(VALUE moduleFFI) rb_define_method(classVariadicInvoker, "initialize", variadic_initialize, 4); rb_define_method(classVariadicInvoker, "invoke", variadic_invoke, 2); - rb_define_method(classVariadicInvoker, "result_type", variadic_result_type, 0); + rb_define_method(classVariadicInvoker, "return_type", variadic_return_type, 0); } diff --git a/lib/ffi/function.rb b/lib/ffi/function.rb index 5996d96..6fff6ae 100644 --- a/lib/ffi/function.rb +++ b/lib/ffi/function.rb @@ -32,8 +32,8 @@ module FFI class Function # Only MRI allows function type queries if private_method_defined?(:type) - def result_type - type.result_type + def return_type + type.return_type end def param_types diff --git a/spec/ffi/function_type_spec.rb b/spec/ffi/function_type_spec.rb index 54e1c48..2f171d8 100644 --- a/spec/ffi/function_type_spec.rb +++ b/spec/ffi/function_type_spec.rb @@ -8,7 +8,7 @@ require File.expand_path(File.join(File.dirname(__FILE__), "spec_helper")) describe "FFI::FunctionType", skip: RUBY_ENGINE != "ruby" do it 'is initialized with return type and a list of parameter types' do function_type = FFI::FunctionType.new(:int, [ :char, :ulong ]) - expect(function_type.result_type).to be == FFI::Type::Builtin::INT + expect(function_type.return_type).to be == FFI::Type::Builtin::INT expect(function_type.param_types).to be == [ FFI::Type::Builtin::CHAR, FFI::Type::Builtin::ULONG ] end diff --git a/spec/ffi/library_spec.rb b/spec/ffi/library_spec.rb index c9542b9..14a8bc1 100644 --- a/spec/ffi/library_spec.rb +++ b/spec/ffi/library_spec.rb @@ -204,7 +204,7 @@ describe "Library" do fun = mod.attached_functions expect(fun.keys).to eq(["bool_return_true"]) expect(fun["bool_return_true"].param_types).to eq([FFI::Type::STRING]) - expect(fun["bool_return_true"].result_type).to eq(FFI::Type::BOOL) + expect(fun["bool_return_true"].return_type).to eq(FFI::Type::BOOL) end end diff --git a/spec/ffi/variadic_spec.rb b/spec/ffi/variadic_spec.rb index 283878b..917212a 100644 --- a/spec/ffi/variadic_spec.rb +++ b/spec/ffi/variadic_spec.rb @@ -40,7 +40,7 @@ describe "Function with variadic arguments" do it "can reveal its return and parameters" do fun = LibTest.attached_functions["testBlockingWRva"] expect(fun.param_types).to eq([FFI::Type::POINTER, FFI::Type::CHAR, FFI::Type::VARARGS]) - expect(fun.result_type).to eq(FFI::Type::INT8) + expect(fun.return_type).to eq(FFI::Type::INT8) end it 'can wrap a blocking function with varargs' do -- cgit v1.2.1 From e880997492ab4d086f47da5eea444e4442fce086 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Thu, 27 Apr 2023 12:51:01 +0200 Subject: Do not run any Ractor specs on ruby-3.0 Shareable instance variables on modules are essential for most Ractor specs. They were introduced in ruby-3.1. --- spec/ffi/spec_helper.rb | 2 +- spec/ffi/struct_spec.rb | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/ffi/spec_helper.rb b/spec/ffi/spec_helper.rb index 63f8b78..3d8a025 100644 --- a/spec/ffi/spec_helper.rb +++ b/spec/ffi/spec_helper.rb @@ -9,7 +9,7 @@ require 'objspace' RSpec.configure do |c| c.filter_run_excluding :broken => true - c.filter_run_excluding( :ractor ) unless defined?(Ractor) + c.filter_run_excluding( :ractor ) unless defined?(Ractor) && RUBY_VERSION >= "3.1" end module TestLibrary diff --git a/spec/ffi/struct_spec.rb b/spec/ffi/struct_spec.rb index 0429dd5..d09b7b6 100644 --- a/spec/ffi/struct_spec.rb +++ b/spec/ffi/struct_spec.rb @@ -467,8 +467,6 @@ module StructSpecsStructTests end it "Can use CallbackInfo struct field in Ractor", :ractor do - skip "sharing structs accross Ractors requires Ruby-3.1" if RUBY_VERSION < "3.1" - res = Ractor.new do s = CallbackMember::TestStruct.new add_proc = lambda { |a, b| a + b } @@ -562,8 +560,6 @@ module StructSpecsStructTests end it "should be usable with Ractor", :ractor do - skip "Using structs in Ractor requires Ruby-3.1" if RUBY_VERSION < "3.1" - class TestStructRactor < FFI::Struct layout :i, :int end -- cgit v1.2.1 From 89d23a5c5c2e9b48c72e0f05c04747691d9f8afe Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Thu, 27 Apr 2023 13:32:34 +0200 Subject: Bump required ruby version to 2.5 And remove support for older rubies. --- .github/workflows/ci.yml | 4 +--- ffi.gemspec | 2 +- spec/ffi/rbx/memory_pointer_spec.rb | 10 ++++------ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58fdbd0..b9e87d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,12 +42,10 @@ jobs: fail-fast: false matrix: os: [ ubuntu, macos, windows ] - ruby: [ 2.3, 2.4, 2.5, 2.6, 2.7, '3.0', 3.1, 3.2, ruby-head, truffleruby-head, jruby-head ] + ruby: [ 2.5, 2.6, 2.7, '3.0', 3.1, 3.2, ruby-head, truffleruby-head, jruby-head ] exclude: - os: windows ruby: truffleruby-head - - os: windows - ruby: 2.3 # compilation fails runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v2 diff --git a/ffi.gemspec b/ffi.gemspec index 0e9d86a..71b5931 100644 --- a/ffi.gemspec +++ b/ffi.gemspec @@ -34,7 +34,7 @@ Gem::Specification.new do |s| s.rdoc_options = %w[--exclude=ext/ffi_c/.*\.o$ --exclude=ffi_c\.(bundle|so)$] s.license = 'BSD-3-Clause' s.require_paths << 'ext/ffi_c' - s.required_ruby_version = '>= 2.3' + s.required_ruby_version = '>= 2.5' s.add_development_dependency 'rake', '~> 13.0' s.add_development_dependency 'rake-compiler', '~> 1.1' s.add_development_dependency 'rake-compiler-dock', '~> 1.0' diff --git a/spec/ffi/rbx/memory_pointer_spec.rb b/spec/ffi/rbx/memory_pointer_spec.rb index 83d77c3..1270c9b 100644 --- a/spec/ffi/rbx/memory_pointer_spec.rb +++ b/spec/ffi/rbx/memory_pointer_spec.rb @@ -110,12 +110,10 @@ describe "MemoryPointer" do end it "allows overwriting of a default typedef" do - begin - FFI.typedef :uint32, :char - expect(FFI.find_type(:char)).to eq(FFI::Type::Builtin::UINT32) - ensure - FFI.typedef FFI::Type::Builtin::CHAR, :char - end + FFI.typedef :uint32, :char + expect(FFI.find_type(:char)).to eq(FFI::Type::Builtin::UINT32) + ensure + FFI.typedef FFI::Type::Builtin::CHAR, :char end it "allows writing a custom typedef" do -- cgit v1.2.1 From 23e88be86d5031649c4df71f75b17f33ab0a4934 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Tue, 2 May 2023 11:52:37 +0200 Subject: Ensure enums are usable in Ractor --- spec/ffi/enum_spec.rb | 14 ++++++++++++++ spec/ffi/library_spec.rb | 13 +++++++++++++ 2 files changed, 27 insertions(+) diff --git a/spec/ffi/enum_spec.rb b/spec/ffi/enum_spec.rb index b8c5b57..1fba2cb 100644 --- a/spec/ffi/enum_spec.rb +++ b/spec/ffi/enum_spec.rb @@ -430,4 +430,18 @@ describe "All enums" do end end.to raise_error(ArgumentError, /duplicate/) end + + it "should be usable in Ractor", :ractor do + res = Ractor.new do + [ + TestEnum1.test_untagged_enum(:c1), + TestEnum3.test_tagged_typedef_enum1(:c1), + TestEnum4.test_tagged_nonint_enum4(0x45), + TestEnum3.enum_type(:enum_type1)[0], + TestEnum4.enum_type(:enum_type6)[0x4242424242424242], + TestEnum4.enum_value(:c3) + ] + end.take + expect( res ).to eq( [0, :c1, :c20, :c1, :c28, 2] ) + end end diff --git a/spec/ffi/library_spec.rb b/spec/ffi/library_spec.rb index 14a8bc1..52a961f 100644 --- a/spec/ffi/library_spec.rb +++ b/spec/ffi/library_spec.rb @@ -5,6 +5,11 @@ require File.expand_path(File.join(File.dirname(__FILE__), "spec_helper")) +module TestEnumValueRactor + extend FFI::Library + enum :something, [:one, :two] +end + describe "Library" do describe ".enum_value" do m = Module.new do @@ -20,6 +25,14 @@ describe "Library" do it "should return nil for an invalid key" do expect(m.enum_value(:three)).to be nil end + + it "should be queryable in Ractor", :ractor do + res = Ractor.new do + TestEnumValueRactor.enum_value(:one) + end.take + + expect( res ).to eq(0) + end end describe "#ffi_convention" do -- cgit v1.2.1 From fe556f176d8239887fb583db2f3bb78f92a8cec2 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Fri, 5 May 2023 23:56:47 +0200 Subject: Freeze immutable objects and add explicit make_shareable to make library definitions Ractor usable Freeze objects which are immutable from the start are now freezed. This allows these objects to be used by Ractor without make_shareable. This partially reverts commit 4fc6a8c5ec8a9a720330946af9d1103015c62942 in such a way, that functions are stored in a module variable @ffi_functions. Enums are implemented per FFI::Library not per attached function. To make them shareable they would have to be copied and freezed per function. This would increase memory footprint for the sake of Ractor support. IMHO it's better to mark the module explicit as finished by .freeze to allow its use in a Ractor. This also enables querying the enum definitions from a Ractor. Using a Hash instead of per-function variables allow to use foo! and bar? methods as wished in #971. Fixes #971 --- ext/ffi_c/DynamicLibrary.c | 7 +++-- ext/ffi_c/Function.c | 12 ++++++--- ext/ffi_c/FunctionInfo.c | 2 ++ ext/ffi_c/MappedType.c | 2 ++ ext/ffi_c/StructLayout.c | 7 +++++ ext/ffi_c/Type.c | 4 +++ ext/ffi_c/Variadic.c | 5 ++-- ext/ffi_c/rbffi.h | 2 +- lib/ffi/library.rb | 36 ++++++++++++++++--------- lib/ffi/struct.rb | 2 +- lib/ffi/struct_layout.rb | 2 +- lib/ffi/struct_layout_builder.rb | 4 +-- lib/ffi/variadic.rb | 7 ++--- spec/ffi/dynamic_library_spec.rb | 6 ++++- spec/ffi/enum_spec.rb | 24 +++++++++++------ spec/ffi/function_spec.rb | 2 +- spec/ffi/library_spec.rb | 57 ++++++++++++++++++++++++++-------------- spec/ffi/struct_by_ref_spec.rb | 2 +- spec/ffi/struct_spec.rb | 16 ++++++++++- spec/ffi/type_spec.rb | 35 +++++++++++++++++++++++- spec/ffi/variadic_spec.rb | 2 ++ 21 files changed, 175 insertions(+), 61 deletions(-) diff --git a/ext/ffi_c/DynamicLibrary.c b/ext/ffi_c/DynamicLibrary.c index 9abafc7..1d83940 100644 --- a/ext/ffi_c/DynamicLibrary.c +++ b/ext/ffi_c/DynamicLibrary.c @@ -161,7 +161,9 @@ library_initialize(VALUE self, VALUE libname, VALUE libflags) library->handle = RTLD_DEFAULT; } #endif - rb_iv_set(self, "@name", libname != Qnil ? libname : rb_str_new2("[current process]")); + rb_iv_set(self, "@name", libname != Qnil ? rb_str_new_frozen(libname) : rb_str_new2("[current process]")); + + rb_obj_freeze(self); return self; } @@ -266,8 +268,9 @@ symbol_new(VALUE library, void* address, VALUE name) sym->base.memory.typeSize = 1; sym->base.memory.flags = MEM_RD | MEM_WR; RB_OBJ_WRITE(obj, &sym->base.rbParent, library); - RB_OBJ_WRITE(obj, &sym->name, name); + RB_OBJ_WRITE(obj, &sym->name, rb_str_new_frozen(name)); + rb_obj_freeze(obj); return obj; } diff --git a/ext/ffi_c/Function.c b/ext/ffi_c/Function.c index 9da6b37..d0b65fb 100644 --- a/ext/ffi_c/Function.c +++ b/ext/ffi_c/Function.c @@ -492,7 +492,7 @@ static VALUE function_attach(VALUE self, VALUE module, VALUE name) { Function* fn; - char var[1024]; + VALUE funcs; StringValue(name); TypedData_Get_Struct(self, Function, &function_data_type, fn); @@ -514,9 +514,13 @@ function_attach(VALUE self, VALUE module, VALUE name) /* * Stash the Function in a module variable so it does not get garbage collected and can be inspected by attached_functions */ - snprintf(var, sizeof(var), "@ffi_function_%s", StringValueCStr(name)); - rb_ractor_make_shareable(self); - rb_iv_set(module, var, self); + + funcs = rb_iv_get(module, "@ffi_functions"); + if (RB_NIL_P(funcs)) { + funcs = rb_hash_new(); + rb_iv_set(module, "@ffi_functions", funcs); + } + rb_hash_aset(funcs, rb_str_intern(name), self); rb_define_singleton_method(module, StringValueCStr(name), rbffi_MethodHandle_CodeAddress(fn->methodHandle), -1); diff --git a/ext/ffi_c/FunctionInfo.c b/ext/ffi_c/FunctionInfo.c index 7495215..b5150d8 100644 --- a/ext/ffi_c/FunctionInfo.c +++ b/ext/ffi_c/FunctionInfo.c @@ -251,6 +251,8 @@ fntype_initialize(int argc, VALUE* argv, VALUE self) fnInfo->invoke = rbffi_GetInvoker(fnInfo); + rb_obj_freeze(fnInfo->rbParameterTypes); + rb_obj_freeze(self); return self; } diff --git a/ext/ffi_c/MappedType.c b/ext/ffi_c/MappedType.c index 20b14e9..2e506f2 100644 --- a/ext/ffi_c/MappedType.c +++ b/ext/ffi_c/MappedType.c @@ -110,6 +110,8 @@ mapped_initialize(VALUE self, VALUE rbConverter) TypedData_Get_Struct(m->rbType, Type, &rbffi_type_data_type, m->type); m->base.ffiType = m->type->ffiType; + rb_obj_freeze(self); + return self; } diff --git a/ext/ffi_c/StructLayout.c b/ext/ffi_c/StructLayout.c index 613394a..a56d48f 100644 --- a/ext/ffi_c/StructLayout.c +++ b/ext/ffi_c/StructLayout.c @@ -181,6 +181,8 @@ struct_field_initialize(int argc, VALUE* argv, VALUE self) break; } + rb_obj_freeze(self); + return self; } @@ -538,6 +540,11 @@ struct_layout_initialize(VALUE self, VALUE fields, VALUE size, VALUE align) rb_raise(rb_eRuntimeError, "Struct size is zero"); } + rb_obj_freeze(layout->rbFieldMap); + rb_obj_freeze(layout->rbFields); + rb_obj_freeze(layout->rbFieldNames); + rb_obj_freeze(self); + return self; } diff --git a/ext/ffi_c/Type.c b/ext/ffi_c/Type.c index 9bf5681..a94c009 100644 --- a/ext/ffi_c/Type.c +++ b/ext/ffi_c/Type.c @@ -128,6 +128,8 @@ type_initialize(VALUE self, VALUE value) rb_raise(rb_eArgError, "wrong type"); } + rb_obj_freeze(self); + return self; } @@ -192,6 +194,8 @@ builtin_type_new(VALUE klass, int nativeType, ffi_type* ffiType, const char* nam type->type.nativeType = nativeType; type->type.ffiType = ffiType; + rb_obj_freeze(obj); + return obj; } diff --git a/ext/ffi_c/Variadic.c b/ext/ffi_c/Variadic.c index 0f321d9..09d7ce8 100644 --- a/ext/ffi_c/Variadic.c +++ b/ext/ffi_c/Variadic.c @@ -183,9 +183,8 @@ variadic_initialize(VALUE self, VALUE rbFunction, VALUE rbParameterTypes, VALUE /* * @fixed and @type_map are used by the parameter mangling ruby code */ - rb_iv_set(self, "@fixed", fixed); - rb_iv_set(self, "@type_map", rb_obj_dup(rb_hash_aref(options, ID2SYM(rb_intern("type_map"))))); - rb_ractor_make_shareable(self); + rb_iv_set(self, "@fixed", rb_obj_freeze(fixed)); + rb_iv_set(self, "@type_map", rb_hash_aref(options, ID2SYM(rb_intern("type_map")))); return retval; } diff --git a/ext/ffi_c/rbffi.h b/ext/ffi_c/rbffi.h index 89b3e32..0e4e91a 100644 --- a/ext/ffi_c/rbffi.h +++ b/ext/ffi_c/rbffi.h @@ -39,7 +39,7 @@ extern "C" { #define MAX_PARAMETERS (32) extern VALUE rbffi_FFIModule; - + extern void rbffi_Type_Init(VALUE ffiModule); extern void rbffi_Buffer_Init(VALUE ffiModule); extern void rbffi_Invoker_Init(VALUE ffiModule); diff --git a/lib/ffi/library.rb b/lib/ffi/library.rb index e681b3e..8af457d 100644 --- a/lib/ffi/library.rb +++ b/lib/ffi/library.rb @@ -295,9 +295,10 @@ module FFI # If it is a global struct, just attach directly to the pointer s = s = type.new(address) # Assigning twice to suppress unused variable warning self.module_eval <<-code, __FILE__, __LINE__ - @ffi_gsvar_#{mname} = s + @ffi_gsvars = {} unless defined?(@ffi_gsvars) + @ffi_gsvars[#{mname.inspect}] = s def self.#{mname} - @ffi_gsvar_#{mname} + @ffi_gsvars[#{mname.inspect}] end code @@ -309,12 +310,13 @@ module FFI # Attach to this module as mname/mname= # self.module_eval <<-code, __FILE__, __LINE__ - @ffi_gvar_#{mname} = s + @ffi_gvars = {} unless defined?(@ffi_gvars) + @ffi_gvars[#{mname.inspect}] = s def self.#{mname} - @ffi_gvar_#{mname}[:gvar] + @ffi_gvars[#{mname.inspect}][:gvar] end def self.#{mname}=(value) - @ffi_gvar_#{mname}[:gvar] = value + @ffi_gvars[#{mname.inspect}][:gvar] = value end code @@ -541,20 +543,30 @@ module FFI end def attached_functions - instance_variables.grep(/\A@ffi_function_(.*)/) do |m| - [$1, instance_variable_get(m)] - end.to_h + @ffi_functions || {} end def attached_variables ( - instance_variables.grep(/\A@ffi_gsvar_(.*)/) do |m| - [$1, instance_variable_get(m).class] + (@ffi_gsvars || {}).map do |name, gvar| + [name, gvar.class] end + - instance_variables.grep(/\A@ffi_gvar_(.*)/) do |m| - [$1, instance_variable_get(m).layout[:gvar].type] + (@ffi_gvars || {}).map do |name, gvar| + [name, gvar.layout[:gvar].type] end ).to_h end + + # Freeze all definitions of the module + # + # This freezes the module's definitions, so that it can be used in a Ractor. + # No further methods or variables can be attached and no further enums or typedefs can be created in this module afterwards. + def freeze + instance_variables.each do |name| + var = instance_variable_get(name) + FFI.make_shareable(var) + end + nil + end end end diff --git a/lib/ffi/struct.rb b/lib/ffi/struct.rb index 1283d63..725b9cb 100644 --- a/lib/ffi/struct.rb +++ b/lib/ffi/struct.rb @@ -219,7 +219,7 @@ module FFI end builder.size = @size if defined?(@size) && @size > builder.size cspec = builder.build - @layout = FFI.make_shareable(cspec) unless self == Struct + @layout = cspec unless self == Struct @size = cspec.size return cspec end diff --git a/lib/ffi/struct_layout.rb b/lib/ffi/struct_layout.rb index d5a78a7..3fd68cb 100644 --- a/lib/ffi/struct_layout.rb +++ b/lib/ffi/struct_layout.rb @@ -80,8 +80,8 @@ module FFI class Mapped < Field def initialize(name, offset, type, orig_field) - super(name, offset, type) @orig_field = orig_field + super(name, offset, type) end def get(ptr) diff --git a/lib/ffi/struct_layout_builder.rb b/lib/ffi/struct_layout_builder.rb index 5488033..d7d26a2 100644 --- a/lib/ffi/struct_layout_builder.rb +++ b/lib/ffi/struct_layout_builder.rb @@ -97,7 +97,7 @@ module FFI # List of number types - NUMBER_TYPES = FFI.make_shareable([ + NUMBER_TYPES = [ Type::INT8, Type::UINT8, Type::INT16, @@ -112,7 +112,7 @@ module FFI Type::FLOAT64, Type::LONGDOUBLE, Type::BOOL, - ]) + ].freeze # @param [String, Symbol] name name of the field # @param [Array, DataConverter, Struct, StructLayout::Field, Symbol, Type] type type of the field diff --git a/lib/ffi/variadic.rb b/lib/ffi/variadic.rb index 800d121..246b52f 100644 --- a/lib/ffi/variadic.rb +++ b/lib/ffi/variadic.rb @@ -54,11 +54,12 @@ module FFI invoker = self params = "*args" call = "call" - mod.module_eval <<-code - @ffi_function_#{mname} = invoker + mod.module_eval <<-code, __FILE__, __LINE__ + @ffi_functions = {} unless defined?(@ffi_functions) + @ffi_functions[#{mname.inspect}] = invoker def self.#{mname}(#{params}) - @ffi_function_#{mname}.#{call}(#{params}) + @ffi_functions[#{mname.inspect}].#{call}(#{params}) end define_method(#{mname.inspect}, &method(:#{mname})) diff --git a/spec/ffi/dynamic_library_spec.rb b/spec/ffi/dynamic_library_spec.rb index a6ea2a9..088a822 100644 --- a/spec/ffi/dynamic_library_spec.rb +++ b/spec/ffi/dynamic_library_spec.rb @@ -9,7 +9,6 @@ describe FFI::DynamicLibrary do it "should be shareable for Ractor", :ractor do libtest = FFI::DynamicLibrary.open(TestLibrary::PATH, FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_GLOBAL) - Ractor.make_shareable(libtest) res = Ractor.new(libtest) do |libtest2| libtest2.find_symbol("testClosureVrV").address @@ -52,5 +51,10 @@ describe FFI::DynamicLibrary do size = ObjectSpace.memsize_of(symbol) expect(size).to be > base_size end + + it "should be shareable for Ractor", :ractor do + symbol = @libtest.find_symbol("gvar_gstruct_set") + expect(Ractor.shareable?(symbol)).to be true + end end end diff --git a/spec/ffi/enum_spec.rb b/spec/ffi/enum_spec.rb index 1fba2cb..7a55b07 100644 --- a/spec/ffi/enum_spec.rb +++ b/spec/ffi/enum_spec.rb @@ -26,14 +26,18 @@ module TestEnum3 ffi_lib TestLibrary::PATH enum :enum_type1, [:c1, :c2, :c3, :c4] - enum :enum_type2, [:c5, 42, :c6, :c7, :c8] - enum :enum_type3, [:c9, 42, :c10, :c11, 4242, :c12] - enum :enum_type4, [:c13, 42, :c14, 4242, :c15, 424242, :c16, 42424242] - attach_function :test_tagged_typedef_enum1, [:enum_type1], :enum_type1 + + enum :enum_type2, [:c5, 42, :c6, :c7, :c8] attach_function :test_tagged_typedef_enum2, [:enum_type2], :enum_type2 + + enum :enum_type3, [:c9, 42, :c10, :c11, 4242, :c12] attach_function :test_tagged_typedef_enum3, [:enum_type3], :enum_type3 + + enum :enum_type4, [:c13, 42, :c14, 4242, :c15, 424242, :c16, 42424242] attach_function :test_tagged_typedef_enum4, [:enum_type4], :enum_type4 + + freeze end module TestEnum4 @@ -44,18 +48,22 @@ module TestEnum4 enum :enum_type1, [:c5, 0x42, :c6, :c7, :c8] enum :enum_type2, [:c9, 0x42, :c10, :c11, 0x4242, :c12] enum :enum_type3, [:c13, 0x42, :c14, 0x4242, :c15, 0x42424242, :c16, 0x4242424242424242] - enum FFI::Type::UINT16, :enum_type4, [:c17, 0x42, :c18, :c19, :c20] - enum FFI::Type::UINT32, :enum_type5, [:c21, 0x42, :c22, :c23, 0x4242, :c24] - enum FFI::Type::UINT64, :enum_type6, [:c25, 0x42, :c26, 0x4242, :c27, 0x42424242, :c28, 0x4242424242424242] - enum FFI::Type::UINT64, [:c29, 0x4242424242424242, :c30, :c31, :c32] attach_function :test_untagged_nonint_enum, [:uint8], :uint8 attach_function :test_tagged_nonint_enum1, [:uint16], :uint16 attach_function :test_tagged_nonint_enum2, [:uint32], :uint32 attach_function :test_tagged_nonint_enum3, [:uint64], :uint64 + + enum FFI::Type::UINT16, :enum_type4, [:c17, 0x42, :c18, :c19, :c20] + enum FFI::Type::UINT32, :enum_type5, [:c21, 0x42, :c22, :c23, 0x4242, :c24] + enum FFI::Type::UINT64, :enum_type6, [:c25, 0x42, :c26, 0x4242, :c27, 0x42424242, :c28, 0x4242424242424242] + enum FFI::Type::UINT64, [:c29, 0x4242424242424242, :c30, :c31, :c32] + attach_function :test_tagged_nonint_enum4, :test_tagged_nonint_enum1, [:enum_type4], :enum_type4 attach_function :test_tagged_nonint_enum5, :test_tagged_nonint_enum2, [:enum_type5], :enum_type5 attach_function :test_tagged_nonint_enum6, :test_tagged_nonint_enum3, [:enum_type6], :enum_type6 + + freeze end describe "A library with no enum defined" do diff --git a/spec/ffi/function_spec.rb b/spec/ffi/function_spec.rb index badd240..77d09d8 100644 --- a/spec/ffi/function_spec.rb +++ b/spec/ffi/function_spec.rb @@ -52,7 +52,7 @@ describe FFI::Function do a + b end - it "should be shareable for Ractor", :ractor do + it "can be made shareable for Ractor", :ractor do add = FFI::Function.new(:int, [:int, :int], &method(:adder)) Ractor.make_shareable(add) diff --git a/spec/ffi/library_spec.rb b/spec/ffi/library_spec.rb index 52a961f..1b8e8c1 100644 --- a/spec/ffi/library_spec.rb +++ b/spec/ffi/library_spec.rb @@ -8,6 +8,7 @@ require File.expand_path(File.join(File.dirname(__FILE__), "spec_helper")) module TestEnumValueRactor extend FFI::Library enum :something, [:one, :two] + freeze end describe "Library" do @@ -197,28 +198,46 @@ describe "Library" do end.getpid).to eq(Process.pid) }.to raise_error(LoadError) end + end - it "attach_function :bool_return_true from [ File.expand_path(#{TestLibrary::PATH.inspect}) ]" do - mod = Module.new do |m| - m.extend FFI::Library - ffi_lib File.expand_path(TestLibrary::PATH) - attach_function :bool_return_true, [ ], :bool - end - expect(mod.bool_return_true).to be true + it "attach_function :bool_return_true from [ File.expand_path(#{TestLibrary::PATH.inspect}) ]" do + mod = Module.new do |m| + m.extend FFI::Library + ffi_lib File.expand_path(TestLibrary::PATH) + attach_function :bool_return_true, [ ], :bool end + expect(mod.bool_return_true).to be true + end - it "can reveal the function type" do - mod = Module.new do |m| - m.extend FFI::Library - ffi_lib File.expand_path(TestLibrary::PATH) - attach_function :bool_return_true, [ :string ], :bool - end + it "can define a foo! function" do + mod = Module.new do |m| + m.extend FFI::Library + ffi_lib File.expand_path(TestLibrary::PATH) + attach_function :foo!, :bool_return_true, [], :bool + end + expect(mod.foo!).to be true + end + + it "can define a foo? function" do + mod = Module.new do |m| + m.extend FFI::Library + ffi_lib File.expand_path(TestLibrary::PATH) + attach_function :foo?, :bool_return_true, [], :bool + end + expect(mod.foo?).to be true + end - fun = mod.attached_functions - expect(fun.keys).to eq(["bool_return_true"]) - expect(fun["bool_return_true"].param_types).to eq([FFI::Type::STRING]) - expect(fun["bool_return_true"].return_type).to eq(FFI::Type::BOOL) + it "can reveal the function type" do + mod = Module.new do |m| + m.extend FFI::Library + ffi_lib File.expand_path(TestLibrary::PATH) + attach_function :bool_return_true, [ :string ], :bool end + + fun = mod.attached_functions + expect(fun.keys).to eq([:bool_return_true]) + expect(fun[:bool_return_true].param_types).to eq([FFI::Type::STRING]) + expect(fun[:bool_return_true].return_type).to eq(FFI::Type::BOOL) end def gvar_lib(name, type) @@ -355,7 +374,7 @@ describe "Library" do ffi_lib TestLibrary::PATH attach_variable :gvari, "gvar_gstruct", GlobalStruct end - expect(lib.attached_variables).to eq({ "gvari" => GlobalStruct }) + expect(lib.attached_variables).to eq({ gvari: GlobalStruct }) end it "can reveal its attached global variables" do @@ -364,7 +383,7 @@ describe "Library" do ffi_lib TestLibrary::PATH attach_variable :gvaro, "gvar_u32", :uint32 end - expect(lib.attached_variables).to eq({ "gvaro" => FFI::Type::UINT32 }) + expect(lib.attached_variables).to eq({ gvaro: FFI::Type::UINT32 }) end it "should have shareable constants for Ractor", :ractor do diff --git a/spec/ffi/struct_by_ref_spec.rb b/spec/ffi/struct_by_ref_spec.rb index 0c01137..f664937 100644 --- a/spec/ffi/struct_by_ref_spec.rb +++ b/spec/ffi/struct_by_ref_spec.rb @@ -41,7 +41,7 @@ describe FFI::Struct, ' by_ref' do end it "can reveal the mapped type converter" do - param_type = @api.attached_functions["struct_test"].param_types[0] + param_type = @api.attached_functions[:struct_test].param_types[0] expect(param_type).to be_a(FFI::Type::Mapped) expect(param_type.converter).to be_a(FFI::StructByReference) end diff --git a/spec/ffi/struct_spec.rb b/spec/ffi/struct_spec.rb index d09b7b6..ab62da1 100644 --- a/spec/ffi/struct_spec.rb +++ b/spec/ffi/struct_spec.rb @@ -545,7 +545,7 @@ module StructSpecsStructTests expect(b.members).to eq([:a, :b]) end - it "should be shareable for Ractor", :ractor do + it "can be made shareable for Ractor", :ractor do a = Class.new(FFI::Struct) do layout :a, :char end.new @@ -1109,6 +1109,20 @@ describe "Struct memsize functions", skip: RUBY_ENGINE != "ruby" do size = ObjectSpace.memsize_of(layout[:pointer]) expect(size).to be > base_size end + + it "StructLayout should be shareable with Ractor", :ractor do + kl = Class.new(FFI::Struct) do + layout :ptr, :pointer + end + expect(Ractor.shareable?(kl.layout)).to eq(true) + end + + it "StructField should be shareable with Ractor", :ractor do + kl = Class.new(FFI::Struct) do + layout :ptr, :pointer + end + expect(Ractor.shareable?(kl.layout[:ptr])).to eq(true) + end end diff --git a/spec/ffi/type_spec.rb b/spec/ffi/type_spec.rb index eb48a43..e880c83 100644 --- a/spec/ffi/type_spec.rb +++ b/spec/ffi/type_spec.rb @@ -5,7 +5,7 @@ require File.expand_path(File.join(File.dirname(__FILE__), "spec_helper")) -describe "FFI::Type" do +describe FFI::Type do it 'has a memsize function', skip: RUBY_ENGINE != "ruby" do base_size = ObjectSpace.memsize_of(Object.new) @@ -40,4 +40,37 @@ describe "FFI::Type" do size = ObjectSpace.memsize_of(FFI::Type::Builtin::CHAR) expect(size).to be > base_size end + + it "should be shareable with Ractor", :ractor do + expect(Ractor.shareable?(FFI::Type.new(5))).to eq(true) + end + + describe :Builtin do + it "should be shareable with Ractor", :ractor do + expect(Ractor.shareable?(FFI::Type::INT32)).to eq(true) + end + end + + describe :Mapped do + it "should be shareable with Ractor", :ractor do + converter = Module.new do + extend FFI::DataConverter + + def self.native_type + FFI::Type::INT32 + end + + def self.to_native(val, ctx) + ToNativeMap[val] + end + + def self.from_native(val, ctx) + FromNativeMap[val] + end + end + expect(Ractor.shareable?(converter)).to eq(true) + type = FFI::Type::Mapped.new(converter) + expect(Ractor.shareable?(type)).to eq(true) + end + end end diff --git a/spec/ffi/variadic_spec.rb b/spec/ffi/variadic_spec.rb index 917212a..21a803f 100644 --- a/spec/ffi/variadic_spec.rb +++ b/spec/ffi/variadic_spec.rb @@ -23,6 +23,8 @@ describe "Function with variadic arguments" do attach_function :testBlockingClose, [ :pointer ], :void attach_function :testCallbackVrDva, :testClosureVrDva, [ :double, :varargs ], :double attach_function :testCallbackVrILva, :testClosureVrILva, [ :int, :long, :varargs ], :long + + freeze end it "takes enum arguments" do -- cgit v1.2.1 From b0ed4717ea08c61d8a1d0c9205cedff3e33805e3 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Sun, 7 May 2023 21:14:36 +0200 Subject: Add documentation to new query methods of commit e987ab50366a4b08617a20568eabdaa1fb761317 --- lib/ffi/function.rb | 10 ++++++++++ lib/ffi/library.rb | 11 +++++++++++ lib/ffi/variadic.rb | 5 +++++ 3 files changed, 26 insertions(+) diff --git a/lib/ffi/function.rb b/lib/ffi/function.rb index 6fff6ae..b4469be 100644 --- a/lib/ffi/function.rb +++ b/lib/ffi/function.rb @@ -32,10 +32,20 @@ module FFI class Function # Only MRI allows function type queries if private_method_defined?(:type) + # Retrieve the return type of the function + # + # This method returns FFI type returned by the function. + # + # @return [FFI::Type] def return_type type.return_type end + # Retrieve Array of parameter types + # + # This method returns an Array of FFI types accepted as function parameters. + # + # @return [Array] def param_types type.param_types end diff --git a/lib/ffi/library.rb b/lib/ffi/library.rb index 8af457d..9394902 100644 --- a/lib/ffi/library.rb +++ b/lib/ffi/library.rb @@ -542,10 +542,21 @@ module FFI end || FFI.find_type(t) end + # Retrieve all attached functions and their function signature + # + # This method returns a Hash of method names of attached functions connected by #attach_function and the corresponding function type. + # The function type responds to #return_type and #param_types which return the FFI types of the function signature. + # + # @return [Hash< Symbol => [FFI::Function, FFI::VariadicInvoker] >] def attached_functions @ffi_functions || {} end + # Retrieve all attached variables and their type + # + # This method returns a Hash of variable names and the corresponding type or variables connected by #attach_variable . + # + # @return [Hash< Symbol => ffi_type >] def attached_variables ( (@ffi_gsvars || {}).map do |name, gvar| diff --git a/lib/ffi/variadic.rb b/lib/ffi/variadic.rb index 246b52f..389c53c 100644 --- a/lib/ffi/variadic.rb +++ b/lib/ffi/variadic.rb @@ -67,6 +67,11 @@ module FFI invoker end + # Retrieve Array of parameter types + # + # This method returns an Array of FFI types accepted as function parameters. + # + # @return [Array] def param_types [*@fixed, Type::Builtin::VARARGS] end -- cgit v1.2.1 From b448759f390bb50eceb9b48a307efeba68c3581c Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Mon, 8 May 2023 08:57:10 +0200 Subject: Register functions in Ruby code For consistency and to ease proting to JRuby and Truffleruby. --- ext/ffi_c/Function.c | 12 ------------ lib/ffi/function.rb | 17 +++++++++++++++++ lib/ffi/library.rb | 1 + lib/ffi/variadic.rb | 3 ++- spec/ffi/library_spec.rb | 2 +- spec/ffi/variadic_spec.rb | 2 +- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/ext/ffi_c/Function.c b/ext/ffi_c/Function.c index d0b65fb..7810056 100644 --- a/ext/ffi_c/Function.c +++ b/ext/ffi_c/Function.c @@ -492,7 +492,6 @@ static VALUE function_attach(VALUE self, VALUE module, VALUE name) { Function* fn; - VALUE funcs; StringValue(name); TypedData_Get_Struct(self, Function, &function_data_type, fn); @@ -511,17 +510,6 @@ function_attach(VALUE self, VALUE module, VALUE name) fn->methodHandle = rbffi_MethodHandle_Alloc(fn->info, fn->base.memory.address); } - /* - * Stash the Function in a module variable so it does not get garbage collected and can be inspected by attached_functions - */ - - funcs = rb_iv_get(module, "@ffi_functions"); - if (RB_NIL_P(funcs)) { - funcs = rb_hash_new(); - rb_iv_set(module, "@ffi_functions", funcs); - } - rb_hash_aset(funcs, rb_str_intern(name), self); - rb_define_singleton_method(module, StringValueCStr(name), rbffi_MethodHandle_CodeAddress(fn->methodHandle), -1); diff --git a/lib/ffi/function.rb b/lib/ffi/function.rb index b4469be..ac4daf0 100644 --- a/lib/ffi/function.rb +++ b/lib/ffi/function.rb @@ -50,5 +50,22 @@ module FFI type.param_types end end + + # Stash the Function in a module variable so it can be inspected by attached_functions. + # On CRuby it also ensures that it does not get garbage collected. + module RegisterAttach + def attach(mod, name) + funcs = mod.instance_variable_get("@ffi_functions") + unless funcs + funcs = {} + mod.instance_variable_set("@ffi_functions", funcs) + end + funcs[name.to_sym] = self + # Jump to the native attach method of CRuby, JRuby or Tuffleruby + super + end + end + private_constant :RegisterAttach + prepend RegisterAttach end end diff --git a/lib/ffi/library.rb b/lib/ffi/library.rb index 9394902..c23112b 100644 --- a/lib/ffi/library.rb +++ b/lib/ffi/library.rb @@ -281,6 +281,7 @@ module FFI # Attach C variable +cname+ to this module. def attach_variable(mname, a1, a2 = nil) cname, type = a2 ? [ a1, a2 ] : [ mname.to_s, a1 ] + mname = mname.to_sym address = nil ffi_libraries.each do |lib| begin diff --git a/lib/ffi/variadic.rb b/lib/ffi/variadic.rb index 389c53c..ee33409 100644 --- a/lib/ffi/variadic.rb +++ b/lib/ffi/variadic.rb @@ -54,6 +54,7 @@ module FFI invoker = self params = "*args" call = "call" + mname = mname.to_sym mod.module_eval <<-code, __FILE__, __LINE__ @ffi_functions = {} unless defined?(@ffi_functions) @ffi_functions[#{mname.inspect}] = invoker @@ -62,7 +63,7 @@ module FFI @ffi_functions[#{mname.inspect}].#{call}(#{params}) end - define_method(#{mname.inspect}, &method(:#{mname})) + define_method(#{mname.inspect}, &method(#{mname.inspect})) code invoker end diff --git a/spec/ffi/library_spec.rb b/spec/ffi/library_spec.rb index 1b8e8c1..9bbbd6c 100644 --- a/spec/ffi/library_spec.rb +++ b/spec/ffi/library_spec.rb @@ -381,7 +381,7 @@ describe "Library" do lib = Module.new do |m| m.extend FFI::Library ffi_lib TestLibrary::PATH - attach_variable :gvaro, "gvar_u32", :uint32 + attach_variable "gvaro", "gvar_u32", :uint32 end expect(lib.attached_variables).to eq({ gvaro: FFI::Type::UINT32 }) end diff --git a/spec/ffi/variadic_spec.rb b/spec/ffi/variadic_spec.rb index 21a803f..4d3f1c2 100644 --- a/spec/ffi/variadic_spec.rb +++ b/spec/ffi/variadic_spec.rb @@ -40,7 +40,7 @@ describe "Function with variadic arguments" do end it "can reveal its return and parameters" do - fun = LibTest.attached_functions["testBlockingWRva"] + fun = LibTest.attached_functions[:testBlockingWRva] expect(fun.param_types).to eq([FFI::Type::POINTER, FFI::Type::CHAR, FFI::Type::VARARGS]) expect(fun.return_type).to eq(FFI::Type::INT8) end -- cgit v1.2.1 From 9ff5782a0129e9025aa20e334ebf1ea77288edf7 Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Mon, 8 May 2023 09:12:50 +0200 Subject: Add Ractor compatibility to README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c798627..574881f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ using Ruby-FFI](https://github.com/ffi/ffi/wiki/why-use-ffi). * C structs (also nested), enums and global variables * Callbacks from C to Ruby * Automatic garbage collection of native memory +* Usable in Ractor ## Synopsis @@ -62,7 +63,7 @@ On JRuby and TruffleRuby, there are no requirements to install the FFI gem, and From rubygems: [sudo] gem install ffi - + From a Gemfile using git or GitHub gem 'ffi', github: 'ffi/ffi', submodules: true -- cgit v1.2.1 From 938b5d8590361acf871fddb9149fe725f65d781f Mon Sep 17 00:00:00 2001 From: Lars Kanis Date: Mon, 8 May 2023 10:44:58 +0200 Subject: Exclude JRuby and Truffleruby at retrieval of function types ..until necessary native methods are added. --- spec/ffi/library_spec.rb | 3 +++ spec/ffi/struct_by_ref_spec.rb | 3 +++ spec/ffi/variadic_spec.rb | 3 +++ 3 files changed, 9 insertions(+) diff --git a/spec/ffi/library_spec.rb b/spec/ffi/library_spec.rb index 9bbbd6c..8691b17 100644 --- a/spec/ffi/library_spec.rb +++ b/spec/ffi/library_spec.rb @@ -228,6 +228,9 @@ describe "Library" do end it "can reveal the function type" do + skip 'this is not yet implemented on JRuby' if RUBY_ENGINE == 'jruby' + skip 'this is not yet implemented on Truffleruby' if RUBY_ENGINE == 'truffleruby' + mod = Module.new do |m| m.extend FFI::Library ffi_lib File.expand_path(TestLibrary::PATH) diff --git a/spec/ffi/struct_by_ref_spec.rb b/spec/ffi/struct_by_ref_spec.rb index f664937..a775f92 100644 --- a/spec/ffi/struct_by_ref_spec.rb +++ b/spec/ffi/struct_by_ref_spec.rb @@ -41,6 +41,9 @@ describe FFI::Struct, ' by_ref' do end it "can reveal the mapped type converter" do + skip 'this is not yet implemented on JRuby' if RUBY_ENGINE == 'jruby' + skip 'this is not yet implemented on Truffleruby' if RUBY_ENGINE == 'truffleruby' + param_type = @api.attached_functions[:struct_test].param_types[0] expect(param_type).to be_a(FFI::Type::Mapped) expect(param_type.converter).to be_a(FFI::StructByReference) diff --git a/spec/ffi/variadic_spec.rb b/spec/ffi/variadic_spec.rb index 4d3f1c2..b528c09 100644 --- a/spec/ffi/variadic_spec.rb +++ b/spec/ffi/variadic_spec.rb @@ -40,6 +40,9 @@ describe "Function with variadic arguments" do end it "can reveal its return and parameters" do + skip 'this is not yet implemented on JRuby' if RUBY_ENGINE == 'jruby' + skip 'this is not yet implemented on Truffleruby' if RUBY_ENGINE == 'truffleruby' + fun = LibTest.attached_functions[:testBlockingWRva] expect(fun.param_types).to eq([FFI::Type::POINTER, FFI::Type::CHAR, FFI::Type::VARARGS]) expect(fun.return_type).to eq(FFI::Type::INT8) -- cgit v1.2.1