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

Class Method Summary collapse

Class Attribute Details

.library_pathObject (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_combObject



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.expand_path(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(options_json)
  load!
  @fn[:bc_client_new].call(options_json)
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.

Raises:



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_pathObject

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

.versionObject

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