diff --git a/watcher-rb/Gemfile b/watcher-rb/Gemfile index d51317c9..0b146682 100644 --- a/watcher-rb/Gemfile +++ b/watcher-rb/Gemfile @@ -4,6 +4,8 @@ source 'https://rubygems.org' gemspec +gem 'ffi' +gem 'fiddle' gem 'rake' gem 'rspec' gem 'solargraph' diff --git a/watcher-rb/Gemfile.lock b/watcher-rb/Gemfile.lock index 475b631d..d609de3a 100644 --- a/watcher-rb/Gemfile.lock +++ b/watcher-rb/Gemfile.lock @@ -2,6 +2,7 @@ PATH remote: . specs: watcher (0.11.0) + ffi GEM remote: https://rubygems.org/ @@ -11,6 +12,8 @@ GEM benchmark (0.3.0) diff-lcs (1.5.1) e2mmap (0.1.0) + ffi (1.17.0-arm64-darwin) + fiddle (1.1.2) jaro_winkler (1.6.0) json (2.7.2) kramdown (2.4.0) @@ -102,6 +105,8 @@ PLATFORMS DEPENDENCIES bundler (~> 2.0) + ffi + fiddle rake rspec solargraph diff --git a/watcher-rb/Rakefile b/watcher-rb/Rakefile index fe407511..23fb9ed2 100644 --- a/watcher-rb/Rakefile +++ b/watcher-rb/Rakefile @@ -1,20 +1,24 @@ # frozen_string_literal: true -require "bundler/gem_tasks" -require "rspec/core/rake_task" +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -task build_shared_library: ["lib"] do - framework_deps = case RbConfig::CONFIG["host_os"] +task build_shared_library: ['lib'] do + case RbConfig::CONFIG['host_os'] when /darwin/ - "-framework CoreServices -framework CoreFoundation" + framework_deps = '-framework CoreServices -framework CoreFoundation' + link_rpath = '-Wl,-rpath,/usr/local/lib' else - "" + framework_deps = '' + link_rpath = '' end - version = File.read("../.version").strip + version = File.read('../.version').strip libname = "libwatcher-c-#{version}.so" - sh "c++ -shared -std=c++17 -O2 #{framework_deps} ../watcher-c/src/watcher-c.cpp -I ../watcher-c/include -o lib/#{libname}" + sources = '../watcher-c/src/watcher-c.cpp' + includes = '../watcher-c/include' + sh "c++ -shared -std=c++17 -O2 #{framework_deps} #{sources} -I #{includes} -o lib/#{libname} #{link_rpath}" end -task default: [:clobber, :build_shared_library, :spec] +task default: %i[clobber build_shared_library spec] diff --git a/watcher-rb/lib/watcher.rb b/watcher-rb/lib/watcher.rb index 9a75d050..800c2f3b 100644 --- a/watcher-rb/lib/watcher.rb +++ b/watcher-rb/lib/watcher.rb @@ -1,57 +1,36 @@ +# rubocop:disable Metrics/MethodLength +# rubocop:disable Style/Documentation # frozen_string_literal: true -require 'fiddle' require 'date' +require 'fiddle' +require 'fiddle/closure' +require 'fiddle/cparser' +require 'fiddle/import' +require 'fiddle/struct' module Watcher - class CEvent < Fiddle::Struct - layout( - :path_name, - :pointer, - :effect_type, - :int8_t, - :path_type, - :int8_t, - :effect_time, - :int64_t, - :associated_path_name, - :pointer - ) - end - - LIB = nil - - def native_solib_file_ending - case RbConfig::CONFIG['host_os'] - when /darwin/ - 'so' - when /mswin|mingw|cygwin/ - 'dll' - else - 'so' - end - end + C_EVENT_DATA = [ + 'char* path_name', + 'int8_t effect_type', + 'int8_t path_type', + 'int64_t effect_time', + 'char* associated_path_name' + ].freeze - def libwatcher_c_lib_path - version = '0.11.0' # hook: tool/release - lib_name = "libwatcher-c-#{version}.#{native_solib_file_ending}" - lib_path = File.join(File.dirname(__FILE__), lib_name) - raise "Library does not exist: '#{lib_path}'" unless File.exist?(lib_path) + C_EVENT = Fiddle::Importer.struct(C_EVENT_DATA) - puts("Using library: '#{lib_path}'") - lib_path - end + C_EVENT_C_TYPE = Fiddle::Importer.parse_struct_signature(C_EVENT_DATA) - def self.lazy_static_solib_handle - return LIB if LIB + # Wraps: void (* wtr_watcher_callback)(struct wtr_watcher_event event, void* context) - @lib = Fiddle.dlopen(libwatcher_c_lib_path) - @lib.extern('void* wtr_watcher_open(char*, void*, void*)') - @lib.extern('bool wtr_watcher_close(void*)') - @lib - end + C_CALLBACK_BRIDGE_CLOSURE = Class.new(Fiddle::Closure) { + def call(c_event, rb_cb) + rb_cb.call(Watcher.c_event_to_event(c_event)) + end + }.new(Fiddle::TYPE_VOID, [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP]) - def to_utf8 + def self.to_utf8 return '' if @value.nil? return @value if @value.is_a?(String) return @value.to_s.force_encoding('UTF-8') if @value.respond_to?(:to_s) @@ -133,8 +112,6 @@ def to_s end class Event - attr_reader :path_name, :effect_type, :path_type, :effect_time, :associated_path_name - def initialize(path_name, effect_type, path_type, effect_time, associated_path_name) @path_name = path_name @effect_type = effect_type @@ -155,23 +132,46 @@ def to_s class Watch def initialize(path, callback) - @lib = Watcher.lazy_static_solib_handle - @path = path.encode('UTF-8') - @callback = callback - @c_callback = Fiddle::Closure::BlockCaller.new(0, [CEvent, :void]) do |c_event, _| - py_event = Watcher.c_event_to_event(c_event) - @callback.call(py_event) - end - - @watcher = @lib.wtr_watcher_open(@path, @c_callback, nil) - raise 'Failed to open a watcher' unless @watcher + native_solib_file_ending = + case RbConfig::CONFIG['host_os'] + when /darwin/ + 'so' + when /mswin|mingw|cygwin/ + 'dll' + else + 'so' + end + version = '0.11.0' # hook: tool/release + lib_name = "libwatcher-c-#{version}.#{native_solib_file_ending}" + lib_path = File.join(File.dirname(__FILE__), lib_name) + puts("Using library: '#{lib_path}'") + @_path = path.encode('UTF-8') + @_callback = callback + @_lib = Fiddle.dlopen(lib_path) + @_wtr_watcher_open = Fiddle::Function.new( + @_lib['wtr_watcher_open'], + [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], + Fiddle::TYPE_VOIDP + ) + @_wtr_watcher_close = Fiddle::Function.new( + @_lib['wtr_watcher_close'], + [Fiddle::TYPE_VOIDP], + Fiddle::TYPE_VOIDP + ) + @_c_callback_bridge = Fiddle::Function.new( + C_CALLBACK_BRIDGE_CLOSURE, + [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], + Fiddle::TYPE_VOID + ) + @_watcher = @_wtr_watcher_open.call(@_path, @_c_callback_bridge, @_callback) + raise 'Failed to open a watcher' unless @_watcher end def close - return unless @watcher + return unless _watcher - @lib.wtr_watcher_close(@watcher) - @watcher = nil + _wtr_watcher_close(_watcher) + _watcher = nil end def finalize @@ -190,3 +190,6 @@ def self.finalize(id) ObjectSpace.define_finalizer(watcher, Watcher::Watch.method(:finalize).to_proc) gets end + +# rubocop:enable Style/Documentation +# rubocop:enable Metrics/MethodLength diff --git a/watcher-rb/watcher.gemspec b/watcher-rb/watcher.gemspec index 715864f5..e5eab503 100644 --- a/watcher-rb/watcher.gemspec +++ b/watcher-rb/watcher.gemspec @@ -17,6 +17,7 @@ Gem::Specification.new do |spec| spec.bindir = 'bin' spec.executables = 'watcher' spec.require_paths = ['lib'] + spec.add_dependency 'ffi' spec.add_development_dependency 'bundler', '~> 2.0' spec.add_development_dependency 'rake', '~> 13.0' spec.add_development_dependency 'rspec', '~> 3.0'