Class: FFI::UCtags

Inherits:
Object
  • Object
show all
Defined in:
lib/ffi/uctags.rb,
lib/ffi/uctags/version.rb,
lib/ffi/uctags/directory.rb

Overview

Auto-load FFI functions and etc. by parsing a C header file. See the README for an overview of the gem with an example.

Most use cases are only concerned with calling the main method UCtags.call and perhaps an UCtags.ffi_module customization. Other class and instance methods (including #initialize) are for advanced uses such as extending the gem. Since instantiating is not intended, ::new has turned private; of course, nothing’s stopping you from un-privatizing it.

Technical developers, you may also be interested in:

Constant Summary collapse

VERSION =

VERSION

'1.1.0'.freeze
EXE_ROOT =

Absolute path to the Universal Ctags root (PREFIX) where the bin and src folders are located.

File.expand_path('../../../../u-ctags/', __FILE__).freeze
EXE_PATH =

Absolute path to the Universal ctags executable – EXE_ROOT/bin/ctags.

File.join(EXE_ROOT, 'bin/ctags').freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(library_name) ⇒ UCtags

Note:

::new is private. See the class description for the intention.

Create an instance for working on the named shared library. The attribute #library is set to a new Library module with the named shared library loaded.

Parameters:

  • library_name (_ToS)


149
150
151
152
153
154
155
156
157
158
159
# File 'lib/ffi/uctags.rb', line 149

def initialize(library_name)
  @library = Module.new #: FFI::library
  @library.extend(ffi_const :Library)
  @library.ffi_lib(library_name)
  
  @composite_types = {}
  @composite_typedefs = {}
  @composite_namespacing = {}
  @stack = []
  @fields = {} # `nil` error prevention
end

Class Attribute Details

.ffi_moduleModule & FFI::Library

Deprecated.

This was originally designed to integrate with alternate FFI implementations such as

The module for call to source constants (namely modules and classes) from; the default is FFI.

The customized module does not have to cover all utilized FFI modules/classes – call will fall back to source from FFI for modules/classes not found from this module (see ffi_const). However, those the module does provide must match in layouts and functionalities as those of FFI.

Nice-FFI or another custom subset of patches. However, the OG FFI library have grown to be a complete platform, to the point that contributing into FFI is more practical than developing mods that may one day go obsolete.

Returns:

  • (Module & FFI::Library)


38
39
40
# File 'lib/ffi/uctags.rb', line 38

def ffi_module
  @ffi_module
end

Instance Attribute Details

#composite_namespacingHash[singleton(FFI::Struct) | FFI::Enum, singleton(FFI::Struct)] (readonly)

A hash that maps inner structs/unions/enums to their outer structs/unions

Returns:

  • (Hash[singleton(FFI::Struct) | FFI::Enum, singleton(FFI::Struct)])


127
128
129
# File 'lib/ffi/uctags.rb', line 127

def composite_namespacing
  @composite_namespacing
end

#composite_typedefsHash[Symbol, singleton(FFI::Struct) | FFI::Enum] (readonly)

Table of typedef-struct/unions/enums

Returns:

  • (Hash[Symbol, singleton(FFI::Struct) | FFI::Enum])


123
124
125
# File 'lib/ffi/uctags.rb', line 123

def composite_typedefs
  @composite_typedefs
end

#composite_typesHash[Symbol, Symbol | singleton(FFI::Struct) | FFI::Enum] (readonly)

A hash that maps struct/union/enum names to either:

  • the class [singleton(FFI::Struct)] or enum [FFI::Enum] directly
  • its (newest) #composite_typedefs key [Symbol], for structs/unions with typedefs.
    • This design allows #const_composites to prefer the (newest) typedef alias over the original, which is often omitted through the typedef-struct and equivalent patterns.

Returns:

  • (Hash[Symbol, Symbol | singleton(FFI::Struct) | FFI::Enum])


119
120
121
# File 'lib/ffi/uctags.rb', line 119

def composite_types
  @composite_types
end

#libraryModule & FFI::Library (readonly)

The Library module this instance is working on

Returns:

  • (Module & FFI::Library)


110
111
112
# File 'lib/ffi/uctags.rb', line 110

def library
  @library
end

#stackArray[[Array[untyped], ^(Array[untyped], String?) -> void, String?] (readonly)

A LIFO array for work-in-progress constructs, most notably functions and structs. The stack design enables building an inner construct (top of the stack) while putting outer constructs on hold.

Each element is a 3-tuple of

  1. a construct member queue
  2. a proc (or equivalent)
  3. the namespace in which this construct should define under When ready, the proc is called with the populated member list (as a single arg) and the namespace.

Returns:

  • (Array[[Array[untyped], ^(Array[untyped], String?) -> void, String?])

See Also:



140
141
142
# File 'lib/ffi/uctags.rb', line 140

def stack
  @stack
end

Class Method Details

.call(library_name, header_path, &blk) ⇒ Module & FFI::Library

Create a new Library module, load the named shared library, and utilize ctags to parse the C header located at header_path.

If providing a block, also evaluate it in the context of the new module (Module#module_eval). Beware that module_eval does not scope constants – you have to retrieve/write them like self::THIS.

Examples:

require 'ffi/uctags'
MyLib = FFI::UCtags.('mylib', 'path/to/mylib.h')
puts MyLib.my_function()

Parameters:

  • library_name (_ToS)
  • header_path (_ToS)

Returns:

  • (Module & FFI::Library)

    the new Library module with every supported construct imported (See the README section for a list of supported constructs)

See Also:



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/ffi/uctags.rb', line 79

def call(library_name, header_path, &blk)
  instance = new(library_name)
  #noinspection SpellCheckingInspection this command use letter flags
  cmd = %W[#{EXE_PATH} --language-force=C --param-CPreProcessor._expand=1 --kinds-C=defgmpstuxz --fields=NFPkSst --fields-C={macrodef} -nuo -] #: Array[_ToS]
  cmd.insert(2, '-V') if $DEBUG
  cmd << header_path
  IO.popen(cmd) do|cmd_out|
    cmd_out.each_line(chomp: true) do|line|
      # Note for maintainers:
      # For compilers’ convenience, C doesn’t allow use before declaration (except for functions pre-C11),
      # so we don’t need to worry about types used before they’re loaded as that’d be the library’s fault.
      name, file, line, k, *fields = line.split("\t")
      line.delete_suffix!(';"')
      puts "processing `#{name}` of kind `#{k}` (#{file}@#{line})" if $VERBOSE
      instance.process(k, name, fields.to_h { _1.split(':', 2) })
    end
  end
  instance.close.tap { _1.module_eval(&blk) if block_given? }
end

.ffi_const(name) ⇒ bot

Deprecated.

This was originally designed to integrate with alternate FFI implementations such as

Look up the named constant from ffi_module or its ancestors, or from FFI if not found in that module.

Parameters:

  • name (Symbol | string)

Returns:

  • (bot)


52
53
54
55
56
# File 'lib/ffi/uctags.rb', line 52

def ffi_const(name)
  ffi_module.const_get(name, true)
rescue NameError
  FFI.const_get(name, true)
end

Instance Method Details

#closeModule & FFI::Library

Note:

it is possible, albeit unorthodox, to continue using this instance after closeing it.

Complete the work of this instance:

  1. Finish up any ongoing progress (see #new_construct)
  2. Assign structs, unions and enums to constants

Returns:



495
496
497
498
499
500
501
502
# File 'lib/ffi/uctags.rb', line 495

def close
  puts 'finishing up' if $VERBOSE
  @fields.clear
  new_construct # flush the last construct
  const_composites
  puts 'done' if $VERBOSE
  library # return
end

#composite_type(name) ⇒ singleton(FFI::Struct) | FFI::Enum

Find the named struct or union (or enum in future versions) from #composite_types.

Parameters:

  • name (String)

Returns:

  • (singleton(FFI::Struct) | FFI::Enum)

Raises:

  • (KeyError)

    if this name is not registered

See Also:



307
308
309
310
311
312
313
314
# File 'lib/ffi/uctags.rb', line 307

def composite_type(name)
  # Find from {#composite_typedefs} first, process if not found
  #noinspection RubyMismatchedReturnType RubyMine cannot follow that `type` can no longer be a Symbol
  composite_typedefs.fetch(name.to_sym) do|name_sym|
    type = composite_types.fetch(name_sym)
    type.is_a?(Symbol) ? composite_typedefs.fetch(type) : type
  end
end

#const_compositesArray[Symbol]

Assign each struct, union or enum in #composite_types to constants.

If the type’s name is invalid (not capitalized), capitalize the first character if possible (e.g., qoi_descQoi_desc), and fall back to prefixing S_, U_ or E_ depending on the type if not. If names collide or the constant is already defined (e.g., due to a previous call to this method), the previous definition is implicitly overridden (with Ruby complaining “already initialized constant”).

Returns:



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
# File 'lib/ffi/uctags.rb', line 454

def const_composites
  union_class = self.ffi_const :Union
  #noinspection RubyMismatchedReturnType RubyMine cannot follow that `type` is a Symbol when set to `name`
  composite_types.map do|name, type|
    # Prefer typedef name
    if type.is_a?(Symbol)
      name = type
      type = composite_typedefs.fetch(type)
    end
    #noinspection RubyMismatchedArgumentType RubyMine cannot follow that `type` can no longer be a Symbol
    namespace = composite_namespacing.fetch(type, @library) #: Module
    puts "\tdefining constant for construct `#{name}`" if $VERBOSE
    begin
      namespace.const_set(name, type)
      name
    rescue NameError # not a capitalized name
      # Capitalize first letter, prefix if cannot
      name = name.to_s
      first_char = name[0]
      name = if first_char&.capitalize! # capitalized
        name[0] = first_char
        name.to_sym
      elsif type.is_a? Class # struct or union
        (type < union_class) ? :"U_#{name}" : :"S_#{name}"
      else # enum (or something else)
        :"E_#{name}"
      end
      puts "\tas `#{name}`" if $VERBOSE
      namespace.const_set(name, type)
      name
    end
  end
end

#extract_and_process_typeFFI::Type

Extract and process (#find_type or #composite_type) the type from @fields (see #process).

Returns:

  • (FFI::Type)

Raises:

  • (TypeError)

    if it’s a basic type with an unrecognized name

  • (KeyError)

    if it’s a struct, union or enum with an unregistered name



321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/ffi/uctags.rb', line 321

def extract_and_process_type
  name, is_composite = extract_type
  if is_composite.nil? # basic type
    find_type(name)
  else
    type = composite_type(name)
    if type.is_a? Class
      is_composite ? type.by_value : type.by_ref
    else
      is_composite ? type : @library.find_type(:pointer)
    end
  end
end

#extract_type[String, bool?]

Extract the type name from @fields (see #process).

Rip off names of types it nests under as all public names in C live in the same global namespace. Identify and processes pointers to and arrays of structs, unions or enums.

Do not process the extracted name to a usable FFI::Type; follow up with #find_type or #composite_type, or use #extract_and_process_type instead.

Returns:

  • ([String, bool?])
    • the name of the extracted type,
    • true if it’s a struct, union or enum, false if it’s a pointer to one of those, or nil if neither.


232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/ffi/uctags.rb', line 232

def extract_type
  type_type, *_, name = @fields.fetch('typeref').split(':')
  if name.end_with?(']')
    puts "\tarray type" if $VERBOSE
    name = 'pointer' # FFI does not support typed array auto-casting for functions
      # (for struct/union members: https://github.com/ParadoxV5/FFI-UCtags/issues/14)
    is_composite = nil
  elsif 'typename'.eql?(type_type) # basic type or typedef
    name_without_star = name.dup
    is_composite = name_without_star.delete_suffix!(' *').nil? # whether pointer suffix not deleted
    if composite_typedefs.include?(name_without_star.to_sym) # typedef-composite
      name = name_without_star
      puts "\ttypedef `#{name}`" if $VERBOSE
    else # basic type
      puts "\tbasic type `#{name}`" if $VERBOSE
      is_composite = nil
    end
  else # non-typedef composite
    puts "\t#{type_type} type `#{name}`" if $VERBOSE
    is_composite = name.delete_suffix!(' *').nil? # whether pointer suffix not deleted
  end
  [name, is_composite]
end

#ffi_constObject

Deprecated.

This was originally designed to integrate with alternate FFI implementations such as

Instance-level delegate for ffi_const



104
# File 'lib/ffi/uctags.rb', line 104

def ffi_const(...) = self.class.ffi_const(...)

#find_type(name) ⇒ FFI::Type

Find the named type from #library.

Find typedefs. Do not find structs, unions and enums; use #composite_type for those. Fall back to TYPE_POINTER for unrecognized unique names.

Parameters:

  • name (String)

Returns:

  • (FFI::Type)

Raises:

  • (TypeError)

    if the basic type is not recognized

See Also:



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/ffi/uctags.rb', line 265

def find_type(name)
  fallback = false
  name_sym = case name
  when /\*/ # `t *`, t (*) []`, `t (*)(…)`, etc.
    :pointer
  when '_Bool'
    :bool
  when 'long double'
    :long_double
  else
    
    # Check multi-keyword integer types (does not match unconventional styles such as `int long untyped long`)
    # duplicate `int_type` capture name is intentional
    if /\A((?<unsigned>un)?signed )?((?<int_type>long|short|long long)( int)?|(?<int_type>int|char))\z/ =~ name
      #noinspection RubyResolve RubyMine cannot extract =~ local vars
      int_type.tr!(' ', '_') # namely `long long` -> 'long_long'
      #noinspection RubyResolve RubyMine cannot extract =~ local vars
      unsigned ? :"u#{int_type}" : int_type.to_sym
    else
      # use type map and fallback
      fallback = true
      name.to_sym
    end
  end
  
  begin
    @library.find_type(name_sym)
  rescue TypeError => e
    raise e unless fallback
    # Assume the unknown type is a pointer alias defined in another file.
    # This should just propagate an exception once multi-file parsing is supported.
    warn "unrecognized type `#{name}`, falling back to `TYPE_POINTER`"
    ffi_const :TYPE_POINTER
  end
end

#new_composite {|members| ... } ⇒ String?

Note:

Does not register the type in #composite_types – caller need to do that separately (structs/unions) or in the block (enums).

Prepare to build a new struct, union or enum.

Yields:

  • a block to build the struct/union/enum once all of the members are in (like with #new_construct, but this method takes care of the namespace block arg)

Yield Parameters:

  • members (Array[untyped])

    the populated member list

Yield Returns:

  • the new struct/union/enum

Returns:

  • (String?)

    The name of the namespace this construct will define under (see #new_construct)



413
414
415
416
417
418
419
# File 'lib/ffi/uctags.rb', line 413

def new_composite(&blk)
  #noinspection RubyMismatchedReturnType RubyMine prefers Yardoc type over RBS type
  new_construct do|members, namespace|
    composite = blk.(members)
    composite_namespacing[composite] = composite_type(namespace) if namespace
  end
end

#new_construct {|members, namespace| ... } ⇒ String?

Prepare to build a new construct. This method is designed for every new construct to call near the beginning.

Array#slice! off topmost entries in the #stack according to @fields. Invoke the procs of the removed entries in reverse order to ensure these previous constructs flush through. Finally, if given a block, start a new stack entry with it.

call processes a composite construct (e.g., a function or struct) as a sequence of consecutive components, which starts with the construct itself followed by its original-ordered list of members (e.g., function params, struct members), all as separate full-sized entries. Therefore, a list must queue the members to compile later until the next sequence commences, especially since these sequences do not have terminator parts nor a member count in the header entry.

Simpler constructs with only one u-ctags entry can simply call this method with no block (“nil block &nil”).

Examples:

new_construct {|members, namespace| library[namespace].build_construct(members) }

Yields:

  • a block to build the desired construct once all of the members are in

Yield Parameters:

  • members (Array[untyped])

    the populated member list

  • namespace (String?)

    the namespace in which the construct should define under

Returns:

  • (String?)

    The name of the namespace this construct will define under as parsed from @fields (see #process)



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/ffi/uctags.rb', line 184

def new_construct(&blk)
  full_namespace = @fields.fetch('struct') { @fields.fetch('union', nil) }
  prev_namespace = nil
  depth = if full_namespace
    full_namespace = full_namespace.split('::')
    prev_namespace = full_namespace.last #: String
    puts "\tunder `#{prev_namespace}`" if $VERBOSE
    full_namespace.size
  else
    0
  end
  if (prev = stack.slice!(depth..)) and not prev.empty?
    puts "\tflushing #{prev.size} stack entries" if $VERBOSE
    prev.reverse_each do|members, a_proc, namespace|
      if $VERBOSE
        puts "\t\twith #{members.size} members"
        puts "\t\tunder `#{namespace}`" if namespace
      end
      a_proc.(members, namespace)
    end
  end
  if blk
    puts "\tstarting new stack entry" if $VERBOSE
    stack << [[], blk, prev_namespace]
  end
  puts "\tstack has #{stack.size} entries" if $VERBOSE
  #noinspection RubyMismatchedReturnType RubyMine prefers Yardoc type over RBS type
  prev_namespace
end

#process(k, name, fields) ⇒ void

Note:

UCtags holds off from creating access points (constants) for structs/unions/enums until calling #const_composites (or #close), as they may later receive a preferred typedef name.

This method returns an undefined value.

Process the u-ctags entry.

This is the controller for processing various u-ctags kinds. Due to its popularity, this stores the argument fields in @fields instead of passing it as an arg when calling helper methods.

For convenience (leading to performance), this method expects entries for composite construct (e.g., a function or struct) to be consecutive. call achieves this by executing u-ctags unsorted, preserving the order from the original file. See #new_construct.

Parameters:

  • k (String)

    one-letter u-ctags kind ID

  • name (String)

    the name of the construct or component; i.e., the u-ctags tag name

  • fields (Hash[String, String])

    additional u-ctags fields (e.g., {'typeref' => 'typename:int'})



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/ffi/uctags.rb', line 352

def process(k, name, fields)
  @fields.replace(fields)
  case k
  # Functions
  when 'z' # function parameters inside function or prototype definitions
    stack_push extract_and_process_type
  when 'p', 'f' # function prototypes, function definitions
    type = extract_and_process_type # check type and fail fast
    new_construct { library.attach_function name, _1, type }
  # Structs/Unions
  when 'm' # struct, and union members
    new_construct
    stack_push name.to_sym, extract_and_process_type
  when 's' # structure names
    struct :Struct, name.to_sym
  when 'u' # union names
    struct :Union, name.to_sym
  # Enums
  when 'e' # enumerators (values inside an enumeration)
    stack_push name.to_sym
  when 'g' # enumeration names
    new_composite { composite_types[name.to_sym] = library.enum(_1) }
  # Miscellaneous
  when 't' # typedefs
    typedef name.to_sym
  when 'd' # macro definitions
    # https://github.com/ParadoxV5/FFI-UCtags/issues/2
  when 'x' # external and forward variable declarations
    new_construct
    @library.attach_variable name, extract_and_process_type
  else
    warn "\tunsupported kind ignored" if $VERBOSE
  end
end

#stack_pushvoid

This method returns an undefined value.

Array#push the given args to the top of the #stack.



217
218
219
# File 'lib/ffi/uctags.rb', line 217

def stack_push(...)
  stack.last&.first&.push(...)
end

#struct(superclass, name) ⇒ singleton(FFI::Struct)

Build and record a new struct or union class

Parameters:

  • superclass (Symbol)

    symbol of the superclass constant (i.e., :Struct or :Union)

  • name (Symbol)

Returns:

  • (singleton(FFI::Struct))


393
394
395
396
397
398
# File 'lib/ffi/uctags.rb', line 393

def struct(superclass, name)
  new_struct = Class.new(ffi_const superclass) #: singleton(FFI::Struct)
  new_composite { new_struct.layout(*_1) }
  #noinspection RubyMismatchedReturnType RubyMine ignores inline RBS annotations
  composite_types[name] = new_struct
end

#typedef(name) ⇒ FFI::Type | singleton(FFI::Struct) | FFI::Enum

Register a typedef. Register in #library directly for basic types; store in #composite_typedefs (and update #composite_types) for structs, unions and enums.

Parameters:

  • name (Symbol)

    the new name

Returns:

  • (FFI::Type | singleton(FFI::Struct) | FFI::Enum)


426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/ffi/uctags.rb', line 426

def typedef(name)
  new_construct
  type_name, is_composite = extract_type
  if is_composite.nil? # basic type
    @library.typedef find_type(type_name), name
  else # composite type
    type = composite_type(type_name)
    if is_composite # configure typedef name only if not aliasing a pointer
      composite_typedefs[name] = type
      composite_types[type_name.to_sym] = name
      type
    elsif type.is_a? Class
      @library.typedef type.by_ref, name
    else
      @library.typedef :pointer, name
    end
  end
end