Class: TreeHaver::Backends::FFI::Language
- Inherits:
-
Object
- Object
- TreeHaver::Backends::FFI::Language
- Includes:
- Comparable
- Defined in:
- lib/tree_haver/backends/ffi.rb
Overview
Represents a tree-sitter language loaded via FFI
Holds a pointer to a TSLanguage struct from a loaded shared library.
Instance Attribute Summary collapse
-
#backend ⇒ Symbol
readonly
The backend this language is for.
-
#path ⇒ String?
readonly
The path this language was loaded from (if known).
-
#pointer ⇒ FFI::Pointer
readonly
The FFI pointer to the TSLanguage struct.
-
#symbol ⇒ String?
readonly
The symbol name (if known).
Class Method Summary collapse
-
.from_library(path, symbol: nil, name: nil) ⇒ Object
(also: from_path)
Instance Method Summary collapse
-
#<=>(other) ⇒ Integer?
Compare languages for equality.
-
#hash ⇒ Integer
Hash value for this language (for use in Sets/Hashes).
-
#initialize(ptr, lib = nil, path: nil, symbol: nil) ⇒ Language
constructor
private
A new instance of Language.
-
#language_name ⇒ Symbol
(also: #name)
Get the language name.
-
#to_ptr ⇒ FFI::Pointer
Convert to FFI pointer for passing to native functions.
Constructor Details
#initialize(ptr, lib = nil, path: nil, symbol: nil) ⇒ Language
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns a new instance of Language.
347 348 349 350 351 352 353 354 355 356 357 358 |
# File 'lib/tree_haver/backends/ffi.rb', line 347 def initialize(ptr, lib = nil, path: nil, symbol: nil) @pointer = ptr @backend = :ffi @path = path @symbol = symbol # Keep a reference to the DynamicLibrary that produced the language # pointer so it isn't garbage-collected and unloaded while the # pointer is still in use by the parser. Not keeping this reference # can lead to the language pointer becoming invalid and causing # segmentation faults when passed to native functions. @library = lib end |
Instance Attribute Details
#backend ⇒ Symbol (readonly)
The backend this language is for
331 332 333 |
# File 'lib/tree_haver/backends/ffi.rb', line 331 def backend @backend end |
#path ⇒ String? (readonly)
The path this language was loaded from (if known)
335 336 337 |
# File 'lib/tree_haver/backends/ffi.rb', line 335 def path @path end |
#pointer ⇒ FFI::Pointer (readonly)
The FFI pointer to the TSLanguage struct
327 328 329 |
# File 'lib/tree_haver/backends/ffi.rb', line 327 def pointer @pointer end |
#symbol ⇒ String? (readonly)
The symbol name (if known)
339 340 341 |
# File 'lib/tree_haver/backends/ffi.rb', line 339 def symbol @symbol end |
Class Method Details
.from_library(path, symbol: nil, name: nil) ⇒ Object Also known as: from_path
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 |
# File 'lib/tree_haver/backends/ffi.rb', line 437 def from_library(path, symbol: nil, name: nil) raise TreeHaver::NotAvailable, "FFI not available" unless Backends::FFI.available? # Check for MRI backend conflict BEFORE loading the grammar # If ruby_tree_sitter has already loaded this grammar file, the dynamic # linker will return the cached library with symbols resolved against # MRI's statically-linked tree-sitter, causing segfaults when FFI # tries to use the pointer with its dynamically-linked libtree-sitter. if defined?(::TreeSitter::Language) # MRI backend has been loaded - check if it might have loaded this grammar # We can't reliably detect which grammars MRI loaded, so we warn and # attempt to proceed. The segfault will occur when setting language on parser. warn("TreeHaver: FFI backend loading grammar after ruby_tree_sitter (MRI backend). " \ "This may cause segfaults due to tree-sitter symbol conflicts. " \ "For reliable operation, use only one backend per process.") if $VERBOSE end # Ensure the core libtree-sitter runtime is loaded first so # the language shared library resolves its symbols against the # same runtime. This prevents cases where the language pointer # is incompatible with the parser (different lib instances). Native.try_load! begin # Prefer resolving symbols immediately and globally so the # language library links to the already-loaded libtree-sitter # (RTLD_NOW | RTLD_GLOBAL). If those constants are not present # fall back to RTLD_LAZY for maximum compatibility. flags = if defined?(::FFI::DynamicLibrary::RTLD_NOW) && defined?(::FFI::DynamicLibrary::RTLD_GLOBAL) ::FFI::DynamicLibrary::RTLD_NOW | ::FFI::DynamicLibrary::RTLD_GLOBAL else ::FFI::DynamicLibrary::RTLD_LAZY end dl = ::FFI::DynamicLibrary.open(path, flags) rescue LoadError, RuntimeError => e # TruffleRuby raises RuntimeError instead of LoadError when a shared library cannot be opened raise TreeHaver::NotAvailable, "Could not open language library at #{path}: #{e.}" end requested = symbol || ENV["TREE_SITTER_LANG_SYMBOL"] # Use shared utility for consistent symbol derivation across backends guessed_symbol = LibraryPathUtils.derive_symbol_from_path(path) # If an override was provided (arg or ENV), treat it as strict and do not fall back. # Only when no override is provided do we attempt guessed and default candidates. candidates = if requested && !requested.to_s.empty? [requested] else [guessed_symbol, "tree_sitter_toml"].compact.uniq end func = nil last_err = nil candidates.each do |name| addr = dl.find_function(name) func = ::FFI::Function.new(:pointer, [], addr) break rescue StandardError => e last_err = e end unless func env_used = [] env_used << "TREE_SITTER_LANG_SYMBOL=#{ENV["TREE_SITTER_LANG_SYMBOL"]}" if ENV["TREE_SITTER_LANG_SYMBOL"] detail = env_used.empty? ? "" : " Env overrides: #{env_used.join(", ")}." raise TreeHaver::NotAvailable, "Could not resolve language symbol in #{path} (tried: #{candidates.join(", ")}).#{detail} #{last_err&.}" end # Only ensure the core lib is loaded when we actually need to interact with it # (e.g., during parsing). Creating the Language handle does not require core to be loaded. ptr = func.call raise TreeHaver::NotAvailable, "Language factory returned NULL for #{path}" if ptr.null? # Pass the opened DynamicLibrary into the Language instance so the # library handle remains alive for the lifetime of the Language. new(ptr, dl, path: path, symbol: symbol) end |
Instance Method Details
#<=>(other) ⇒ Integer?
Compare languages for equality
FFI languages are equal if they have the same backend, path, and symbol.
Path and symbol uniquely identify a loaded language.
367 368 369 370 371 372 373 374 375 376 |
# File 'lib/tree_haver/backends/ffi.rb', line 367 def <=>(other) return unless other.is_a?(Language) return unless other.backend == @backend # Compare by path first, then symbol cmp = (@path || "") <=> (other.path || "") return cmp if cmp.nonzero? (@symbol || "") <=> (other.symbol || "") end |
#hash ⇒ Integer
Hash value for this language (for use in Sets/Hashes)
380 381 382 |
# File 'lib/tree_haver/backends/ffi.rb', line 380 def hash [@backend, @path, @symbol].hash end |
#language_name ⇒ Symbol Also known as: name
Get the language name
Derives a name from the symbol or path.
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 |
# File 'lib/tree_haver/backends/ffi.rb', line 392 def language_name # Try to derive from symbol (e.g., "tree_sitter_toml" -> :toml) if @symbol name = @symbol.to_s.sub(/^tree_sitter_/, "") return name.to_sym end # Try to derive from path (e.g., "/path/to/libtree-sitter-toml.so" -> :toml) if @path name = LibraryPathUtils.derive_language_name_from_path(@path) return name.to_sym if name end :unknown end |
#to_ptr ⇒ FFI::Pointer
Convert to FFI pointer for passing to native functions
414 415 416 |
# File 'lib/tree_haver/backends/ffi.rb', line 414 def to_ptr @pointer end |