Class: TreeHaver::Backends::FFI::Language

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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.

Parameters:

  • ptr (FFI::Pointer)

    pointer to TSLanguage

  • lib (FFI::DynamicLibrary, nil) (defaults to: nil)

    the opened dynamic library
    (kept as an instance variable to prevent it being GC’d/unloaded)

  • path (String, nil) (defaults to: nil)

    path language was loaded from

  • symbol (String, nil) (defaults to: nil)

    symbol name



346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/tree_haver/backends/ffi.rb', line 346

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

#backendSymbol (readonly)

The backend this language is for

Returns:

  • (Symbol)


330
331
332
# File 'lib/tree_haver/backends/ffi.rb', line 330

def backend
  @backend
end

#pathString? (readonly)

The path this language was loaded from (if known)

Returns:

  • (String, nil)


334
335
336
# File 'lib/tree_haver/backends/ffi.rb', line 334

def path
  @path
end

#pointerFFI::Pointer (readonly)

The FFI pointer to the TSLanguage struct

Returns:

  • (FFI::Pointer)


326
327
328
# File 'lib/tree_haver/backends/ffi.rb', line 326

def pointer
  @pointer
end

#symbolString? (readonly)

The symbol name (if known)

Returns:

  • (String, nil)


338
339
340
# File 'lib/tree_haver/backends/ffi.rb', line 338

def symbol
  @symbol
end

Class Method Details

.from_library(path, symbol: nil, name: nil) ⇒ Object Also known as: from_path



436
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
# File 'lib/tree_haver/backends/ffi.rb', line 436

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.message}"
  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&.message}"
  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.

Parameters:

  • other (Object)

    object to compare with

Returns:

  • (Integer, nil)

    -1, 0, 1, or nil if not comparable



366
367
368
369
370
371
372
373
374
375
# File 'lib/tree_haver/backends/ffi.rb', line 366

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

#hashInteger

Hash value for this language (for use in Sets/Hashes)

Returns:

  • (Integer)


379
380
381
# File 'lib/tree_haver/backends/ffi.rb', line 379

def hash
  [@backend, @path, @symbol].hash
end

#language_nameSymbol Also known as: name

Get the language name

Derives a name from the symbol or path.

Returns:

  • (Symbol)

    language name



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/tree_haver/backends/ffi.rb', line 391

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_ptrFFI::Pointer

Convert to FFI pointer for passing to native functions

Returns:

  • (FFI::Pointer)


413
414
415
# File 'lib/tree_haver/backends/ffi.rb', line 413

def to_ptr
  @pointer
end