Module: Beachcomber::FFI
- Defined in:
- lib/beachcomber/ffi.rb
Overview
Loads libbeachcomber and exposes the bc_* C ABI as callable Fiddle::Function objects, plus JSON-envelope decoding shared by Client/Session/WatchStream.
Discovery order (the shared contract every dynamic-language binding follows):
1. $BEACHCOMBER_LIB
2. ../lib/<libname> relative to the resolved `comb` on $PATH
3. the platform default dynamic-linker search path
../lib/ beside comb is checked before the system path deliberately:
library and binary ship together, so the copy next to the comb you
would actually run is the matching one, and a stale system-wide copy
must not win.
A failure to find or load the library, or a missing required symbol, is a loud error naming every location tried (or the missing symbol) plus the loaded library's bc_version() where known. There is no silent fallback to a subprocess transport.
Constant Summary collapse
- LIB_BASENAME =
case RbConfig::CONFIG['host_os'] when /darwin/i 'libbeachcomber.dylib' when /linux/i 'libbeachcomber.so' else raise Beachcomber::Error, "unsupported platform: #{RbConfig::CONFIG['host_os']}" end
- REQUIRED_SYMBOLS =
The 22 bc_* symbols this binding calls, checked at load (not on first use) so a version-skewed or partial install fails loudly up front.
%w[ bc_version bc_client_new bc_client_free bc_string_free bc_get bc_put bc_put_null bc_refresh bc_status bc_introspect bc_hello bc_resolve bc_eval bc_session_open bc_session_close bc_session_get bc_session_put bc_session_set_context bc_watch_open bc_watch_next bc_watch_cancel bc_watch_free ].freeze
- GET_FORCE =
1 << 0
- GET_WAIT =
1 << 1
- VOIDP =
Fiddle::TYPE_VOIDP
- INT =
Fiddle::TYPE_INT
- VOID =
Fiddle::TYPE_VOID
Class Attribute Summary collapse
-
.library_path ⇒ Object
readonly
Returns the value of attribute library_path.
Class Method Summary collapse
-
.call!(sym, *args) ⇒ Object
Like raw_call, but decodes the "ok":... envelope and raises the idiomatic exception for ok:false, returning only the op's
dataon success. - .cancel_watch(handle) ⇒ Object
- .candidate_beside_comb ⇒ Object
- .close_session(handle) ⇒ Object
-
.free_client(handle) ⇒ Object
Void-returning teardown calls (null-safe on the C side; harmless if called more than once from a Ruby finalizer racing an explicit close).
- .free_watch(handle) ⇒ Object
-
.load! ⇒ Object
Loads the library (idempotent) and returns the Fiddle::Handle.
-
.new_client(options_json) ⇒ Object
Opaque-handle constructors.
- .new_session(client_handle) ⇒ Object
- .new_watch(client_handle, key, path) ⇒ Object
-
.raw_call(sym, *args) ⇒ Object
Calls a void*(...)-returning bc_* function, reads the NUL-terminated JSON result, frees it via bc_string_free, and returns the raw JSON string.
-
.resolved_comb_path ⇒ Object
Finds
combon $PATH the way a shell would, resolving symlinks (a Homebrew-linked binary, for instance) so../lib/is computed relative to where the binary actually lives. -
.version ⇒ Object
The loaded library's build version.
Class Attribute Details
.library_path ⇒ Object (readonly)
Returns the value of attribute library_path.
55 56 57 |
# File 'lib/beachcomber/ffi.rb', line 55 def library_path @library_path end |
Class Method Details
.call!(sym, *args) ⇒ Object
Like raw_call, but decodes the "ok":... envelope and raises the
idiomatic exception for ok:false, returning only the op's data on
success. Not for bc_watch_next, whose envelope has a different shape.
212 213 214 215 216 217 218 219 |
# File 'lib/beachcomber/ffi.rb', line 212 def self.call!(sym, *args) envelope = JSON.parse(raw_call(sym, *args)) unless envelope['ok'] err = envelope['error'] || {} Beachcomber.raise_for_error(err['kind'] || 'server_error', err['message'] || 'unknown error') end envelope['data'] end |
.cancel_watch(handle) ⇒ Object
251 252 253 254 |
# File 'lib/beachcomber/ffi.rb', line 251 def self.cancel_watch(handle) load! @fn[:bc_watch_cancel].call(handle) end |
.candidate_beside_comb ⇒ Object
75 76 77 78 79 80 |
# File 'lib/beachcomber/ffi.rb', line 75 def self.candidate_beside_comb comb = resolved_comb_path return nil unless comb File.(File.join(File.dirname(comb), '..', 'lib', LIB_BASENAME)) end |
.close_session(handle) ⇒ Object
246 247 248 249 |
# File 'lib/beachcomber/ffi.rb', line 246 def self.close_session(handle) load! @fn[:bc_session_close].call(handle) end |
.free_client(handle) ⇒ Object
Void-returning teardown calls (null-safe on the C side; harmless if called more than once from a Ruby finalizer racing an explicit close).
241 242 243 244 |
# File 'lib/beachcomber/ffi.rb', line 241 def self.free_client(handle) load! @fn[:bc_client_free].call(handle) end |
.free_watch(handle) ⇒ Object
256 257 258 259 |
# File 'lib/beachcomber/ffi.rb', line 256 def self.free_watch(handle) load! @fn[:bc_watch_free].call(handle) end |
.load! ⇒ Object
Loads the library (idempotent) and returns the Fiddle::Handle.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/beachcomber/ffi.rb', line 83 def self.load! return @handle if defined?(@handle) && @handle tried = [] env_lib = ENV['BEACHCOMBER_LIB'] if env_lib && !env_lib.empty? tried << env_lib handle = try_open(env_lib) return finish_load!(handle, env_lib) if handle end candidate = candidate_beside_comb if candidate tried << candidate handle = try_open(candidate) return finish_load!(handle, candidate) if handle end tried << "#{LIB_BASENAME} (platform default search path)" handle = try_open(LIB_BASENAME) return finish_load!(handle, LIB_BASENAME) if handle raise Beachcomber::LibraryNotFound, "could not locate #{LIB_BASENAME}; tried: #{tried.join(', ')}" end |
.new_client(options_json) ⇒ Object
Opaque-handle constructors. These return a raw pointer (BcClient*, BcSession*, BcWatch*), not a JSON envelope — never NULL except BcWatch* on allocation failure.
224 225 226 227 |
# File 'lib/beachcomber/ffi.rb', line 224 def self.new_client() load! @fn[:bc_client_new].call() end |
.new_session(client_handle) ⇒ Object
229 230 231 232 |
# File 'lib/beachcomber/ffi.rb', line 229 def self.new_session(client_handle) load! @fn[:bc_session_open].call(client_handle) end |
.new_watch(client_handle, key, path) ⇒ Object
234 235 236 237 |
# File 'lib/beachcomber/ffi.rb', line 234 def self.new_watch(client_handle, key, path) load! @fn[:bc_watch_open].call(client_handle, key, path) end |
.raw_call(sym, *args) ⇒ Object
Calls a void*(...)-returning bc_* function, reads the NUL-terminated JSON result, frees it via bc_string_free, and returns the raw JSON string. Not for bc_version, whose result must never be freed.
198 199 200 201 202 203 204 205 206 207 |
# File 'lib/beachcomber/ffi.rb', line 198 def self.raw_call(sym, *args) load! fn = @fn.fetch(sym) { raise ArgumentError, "unknown bc_* function #{sym}" } ptr = fn.call(*args) raise Beachcomber::Error, "unexpected NULL pointer from #{sym}" if ptr.nil? || ptr.null? json = ptr.to_s @fn[:bc_string_free].call(ptr) json end |
.resolved_comb_path ⇒ Object
Finds comb on $PATH the way a shell would, resolving symlinks (a
Homebrew-linked binary, for instance) so ../lib/ is computed
relative to where the binary actually lives.
61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/beachcomber/ffi.rb', line 61 def self.resolved_comb_path ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).each do |dir| next if dir.nil? || dir.empty? candidate = File.join(dir, 'comb') next unless File.file?(candidate) && File.executable?(candidate) return File.realpath(candidate) end nil rescue Errno::ENOENT, Errno::EACCES nil end |
.version ⇒ Object
The loaded library's build version. Static string; never freed.
189 190 191 192 193 |
# File 'lib/beachcomber/ffi.rb', line 189 def self.version load! ptr = @fn[:bc_version].call ptr.null? ? '' : ptr.to_s end |