Class: Snow::CStruct

Inherits:
Object
  • Object
show all
Defined in:
lib/snow-data/c_struct.rb,
lib/snow-data/c_struct/builder.rb,
lib/snow-data/c_struct/array_base.rb,
lib/snow-data/c_struct/struct_base.rb

Defined Under Namespace

Modules: StructArrayBase, StructBase Classes: Builder, StructMemberInfo

Constant Summary collapse

ENCODING_REGEX =

The encoding regex. Just used to make reading encoding strings easy. Do not touch this. – Ordinarily I’d write a lexer for this sort of thing, but regex actually seems to work fine.

TODO: At any rate, replace this with a lexer/parser. At least that way it’ll be possible to provide validation for encodings. ++

%r{
    (?<name>                                      # 0
      [_a-zA-Z][_a-zA-Z\d]*
    )
    \s* \: \s*
    (?<type>                                      # 1
      # Named struct type encoding - must match previously defined type
      \* | [a-zA-Z_][a-zA-Z_0-9]*
    )
    (?<type_array_decl> \s* \[ \s*                # 2
      (?<type_array_count> \d+ )                  # 3
    \s* \] )?
    (?<type_alignment_decl> \s* \: \s*            # 4
      (?<type_alignment> \d+ )                    # 5
      )?
  \s* (?: ; | $ | \n) # terminator
}mx
ALIGNMENTS =

Alignemnts for default types.

{
  :char                 => 1,
  :signed_char          => 1,
  :unsigned_char        => 1,
  :uint8_t              => Memory::SIZEOF_UINT8_T,
  :int8_t               => Memory::SIZEOF_INT8_T,
  :short                => Memory::SIZEOF_SHORT,
  :unsigned_short       => Memory::SIZEOF_SHORT,
  :uint16_t             => Memory::SIZEOF_UINT16_T,
  :int16_t              => Memory::SIZEOF_INT16_T,
  :int32_t              => Memory::SIZEOF_INT32_T,
  :uint32_t             => Memory::SIZEOF_UINT32_T,
  :uint64_t             => Memory::SIZEOF_UINT64_T,
  :int64_t              => Memory::SIZEOF_INT64_T,
  :unsigned_long        => Memory::SIZEOF_LONG,
  :unsigned_long_long   => Memory::SIZEOF_LONG_LONG,
  :long                 => Memory::SIZEOF_LONG,
  :long_long            => Memory::SIZEOF_LONG_LONG,
  :int                  => Memory::SIZEOF_INT,
  :unsigned_int         => Memory::SIZEOF_INT,
  :float                => Memory::SIZEOF_FLOAT,
  :double               => Memory::SIZEOF_DOUBLE,
  :size_t               => Memory::SIZEOF_SIZE_T,
  :ptrdiff_t            => Memory::SIZEOF_PTRDIFF_T,
  :intptr_t             => Memory::SIZEOF_INTPTR_T,
  :uintptr_t            => Memory::SIZEOF_UINTPTR_T
}
SIZES =

Sizes of default types.

{
  :char                 => 1,
  :signed_char          => 1,
  :unsigned_char        => 1,
  :uint8_t              => Memory::SIZEOF_UINT8_T,
  :int8_t               => Memory::SIZEOF_INT8_T,
  :short                => Memory::SIZEOF_SHORT,
  :unsigned_short       => Memory::SIZEOF_SHORT,
  :uint16_t             => Memory::SIZEOF_UINT16_T,
  :int16_t              => Memory::SIZEOF_INT16_T,
  :int32_t              => Memory::SIZEOF_INT32_T,
  :uint32_t             => Memory::SIZEOF_UINT32_T,
  :uint64_t             => Memory::SIZEOF_UINT64_T,
  :int64_t              => Memory::SIZEOF_INT64_T,
  :unsigned_long        => Memory::SIZEOF_LONG,
  :unsigned_long_long   => Memory::SIZEOF_LONG_LONG,
  :long                 => Memory::SIZEOF_LONG,
  :long_long            => Memory::SIZEOF_LONG_LONG,
  :int                  => Memory::SIZEOF_INT,
  :unsigned_int         => Memory::SIZEOF_INT,
  :float                => Memory::SIZEOF_FLOAT,
  :double               => Memory::SIZEOF_DOUBLE,
  :size_t               => Memory::SIZEOF_SIZE_T,
  :ptrdiff_t            => Memory::SIZEOF_PTRDIFF_T,
  :intptr_t             => Memory::SIZEOF_INTPTR_T,
  :uintptr_t            => Memory::SIZEOF_UINTPTR_T
}
TYPE_ALIASES =

Used for getters/setters on Memory objects. Simply maps short type names to their long-form type names.

{
  # char
  :c      => :char,
  :sc     => :signed_char,
  :uc     => :unsigned_char,
  :ui8    => :uint8_t,
  :i8     => :int8_t,
  # short (uint16_t)
  :s      => :short,
  :us     => :unsigned_short,
  :ui16   => :uint16_t,
  :i16    => :int16_t,
  # int32
  :i32    => :int32_t,
  :ui32   => :uint32_t,
  :ui64   => :uint64_t,
  :i64    => :int64_t,
  :ul     => :unsigned_long,
  :ull    => :unsigned_long_long,
  :l      => :long,
  :ll     => :long_long,
  :i      => :int,
  :ui     => :unsigned_int,
  :f      => :float,
  :d      => :double,
  :zu     => :size_t,
  :td     => :ptrdiff_t,
  :ip     => :intptr_t,
  :uip    => :uintptr_t,
  :*      => :intptr_t # pointers always stored at intptr_t
}
@@long_inspect =

Whether long inspect strings are enabled. See both ::long_inspect= and ::long_inspect for accessors.

false

Class Method Summary collapse

Class Method Details

.add_type(name = nil, klass) ⇒ Object

call-seq:

add_type(name, klass) => klass

Adds a type as a possible member type for structs. Types registered this way can be used as a member type by using the name provided to #add_type in struct encodings.



252
253
254
255
256
257
258
259
260
261
262
263
264
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
# File 'lib/snow-data/c_struct.rb', line 252

def self.add_type(name = nil, klass)
  raise "Class must be a subclass of #{Memory}" unless Memory > klass

  if ! name
    name = klass.name
    if (last_sro = name.rindex('::'))
      name = name[last_sro + 2, name.length]
    end
  end

  name = name.to_sym

  raise "Type for #{name} is already defined" if SIZES.include?(name)

  ALIGNMENTS[name] = klass::ALIGNMENT
  SIZES[name]      = klass::SIZE

  getter = :"get_#{name}"
  setter = :"set_#{name}"

  Memory.class_exec do

    define_method(getter) do |offset|
      wrapper = klass.__wrap__(self.address + offset, klass::SIZE, klass::ALIGNMENT)
      wrapper.instance_variable_set(:@__base_memory__, self)
      wrapper
    end # getter

    define_method(setter) do |offset, data|
      raise "Invalid value type, must be Data, but got #{data.class}" if ! data.kind_of?(Data)
      local_addr = self.address + offset
      if !data.respond_to?(:address) || local_addr != data.address
        copy!(data, offset, 0, klass::SIZE)
      end

      data
    end # setter

  end # class_exec

  Builder.flush_type_methods!

  klass
end

.alias_type(new_name, old_name) ⇒ Object

Aliases the type for old_name to new_name. Raises



228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/snow-data/c_struct.rb', line 228

def self.alias_type(new_name, old_name)
  return self if new_name == old_name
  old_name = real_type_of(old_name)

  if ! SIZES.include?(old_name)
    raise ArgumentError, "There is no type named #{old_name} to alias"
  elsif TYPE_ALIASES.include?(new_name) || SIZES.include?(new_name)
    raise ArgumentError, "Type <#{new_name}> is already defined in CStruct"
  end

  TYPE_ALIASES[new_name] = old_name
  Builder.flush_type_methods!
  self
end

.build_array_type(struct_klass) ⇒ Object

:nodoc: Generates an array class for the given struct class. This is called by ::build_struct_type and so shouldn’t be called manually.



552
553
554
555
556
557
558
559
560
# File 'lib/snow-data/c_struct.rb', line 552

def self.build_array_type(struct_klass)
  Class.new(Memory) do |array_klass|
    const_set(:BASE, struct_klass)

    private :realloc!

    include StructArrayBase
  end # Class.new
end

.build_struct_type(members) ⇒ Object

call-seq:

build_struct_type(members) => Class

Builds a struct type for the given array of StructMemberInfo objects. The array of member objects must not be empty.

Raises:

  • (ArgumentError)


491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
# File 'lib/snow-data/c_struct.rb', line 491

def self.build_struct_type(members)
  raise ArgumentError, "Members array must not be empty" if members.empty?

  # Make a copy of the members array so we can store a frozen version of it
  # in the new struct class.
  members = Marshal.load(Marshal.dump(members))
  members.map! { |info| info.freeze }
  members.freeze

  # Get the alignment, size, aligned size, and encoding of the struct.
  alignment    = members.map { |member| member.alignment }.max { |lhs, rhs| lhs <=> rhs }
  type_size    = members.last.size + members.last.offset
  aligned_size = Memory.align_size(type_size, alignment)
  # Oddly enough, it would be easier to pass the encoding string into this
  # function, but then it would ruin the nice little thing I have going where
  # this function isn't dependent on parsing encodings, so we reproduce the
  # encoding here as though it wasn't sitting just above us in the stack
  # (which it might not be, but the chance of it is slim to none).
  encoding = encode_member_info(members).freeze

  Class.new(Memory) do |struct_klass|
    # Set the class's constants, then include StructBase to define its members
    # and other methods.
    const_set(:ENCODING,      encoding)
    const_set(:MEMBERS,       members)
    const_set(:SIZE,          type_size)
    const_set(:ALIGNED_SIZE,  aligned_size)
    const_set(:ALIGNMENT,     alignment)

    const_set(:MEMBERS_HASH,  members.reduce({}) { |hash, member|
      hash[member.name] = member
      hash
    })

    const_set(:MEMBERS_GETFN, members.reduce({}) { |hash, member|
      hash[member.name] = :"get_#{member.name}"
      hash
    })

    const_set(:MEMBERS_SETFN, members.reduce({}) { |hash, member|
      hash[member.name] = :"set_#{member.name}"
      hash
    })


    private :realloc!

    include StructBase

    # Build and define the struct type's array class.
    const_set(:Array, CStruct.build_array_type(self))
  end

end

.decode_member_info(encoding) ⇒ Object

Decodes an encoding string and returns an array of StructMemberInfo objects describing the members of a struct for the given encoding. You may then pass this array to build_struct_type to create a new struct class or encode_member_info to get an encoding string for the encoding string you just decoded, as though that were useful to you somehow.



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'lib/snow-data/c_struct.rb', line 454

def self.decode_member_info(encoding)
  total_size = 0
  encoding.scan(ENCODING_REGEX).map do
    |match|
    name        = match[0].intern
    type        = real_type_of(match[1].intern)
    length      = (match[3] || 1).to_i
    align       = (match[5] || ALIGNMENTS[type] || 1).to_i
    size        = SIZES[type] * length
    offset      = Memory.align_size(total_size, align)
    total_size  = offset + size

    StructMemberInfo[name, type, size, length, align, offset]
  end
end

.encode_member_info(members) ⇒ Object

Given an array of StructMemberInfo objects, returns a valid encoding string for those objects in the order they’re specified in the array. The info objects’ offsets are ignored, as these cannot be specified using an encoding string.



477
478
479
480
481
# File 'lib/snow-data/c_struct.rb', line 477

def self.encode_member_info(members)
  members.map { |member|
    "#{member.name}:#{member.type}[#{member.length}]:#{member.alignment}"
  }.join(?;)
end

.long_inspectObject

call-seq:

long_inspect => boolean

Returns whether long_inspect is enabled. By default, it is disabled.



53
54
55
# File 'lib/snow-data/c_struct.rb', line 53

def self.long_inspect
  @@long_inspect
end

.long_inspect=(enabled) ⇒ Object

call-seq:

long_inspect = boolean => boolean

Sets whether long inspect strings are enabled. By default, they are disabled.

Long inspect strings can be useful for debugging, sepcially if you want to see the value, length, and alignment of every struct member in inspect strings. Otherwise, you can safely leave this disabled.



42
43
44
# File 'lib/snow-data/c_struct.rb', line 42

def self.long_inspect=(enabled)
  @@long_inspect = !!enabled
end

.member_encoding(name, type, length: 1, alignment: nil) ⇒ Object

call-seq:

member_encoding(name, type, length: 1, alignment: nil) => String

Returns an encoding for a struct member with the given name, type, length, and alignment. The type must be a string or symbol, not a Class or other object. If no alignment is provided, it uses the default alignment for the type or the size of a pointer if no alignment can be found.

#### Example

CStruct.member_encoding(:foo, :float, 32, nil) # => "foo:float[32]:4"

Raises:

  • (ArgumentError)


81
82
83
84
85
86
87
# File 'lib/snow-data/c_struct.rb', line 81

def self.member_encoding(name, type, length: 1, alignment: nil)
  type = type.intern
  alignment = alignment || ALIGNMENTS[type] || ALIGNMENTS[:*]
  raise ArgumentError, "Invalid length: #{length}. Must be > 0." if length < 1
  raise ArgumentError, "Invalid alignment: #{alignment}. Must be a power of two." if ! power_of_two?(alignment)
  "#{name}:#{type}[#{length}]:#{alignment}"
end

.new(*args, &block) ⇒ Object Also known as: []

call-seq:

new(name, encoding) => Class
new(encoding) => Class
new(name) { ... } => Class
new { ... } => Class

Creates a new C-struct class and returns it. Optionally, if a name is provided, it is also added as a class under the CStruct class.

In the first form when a name is provided, the name must be valid for a constant and be unique among CStruct types. The resulting type will be set as a constant under the CStruct class. So, for example:

CStruct.new(:SomeStruct, 'member: float')       # => Snow::CStruct::SomeStruct
CStruct::SomeStruct.new                         # => <Snow::CStruct::SomeStruct:...>

Additionally, this will register it as a possible member type for other structs, though struct types must be defined before they are used in other structs, otherwise there is no data for determining size, alignment, and so on for those structs and as such will likely result in an error.

If no name is provided, the new class isn’t set as a constant or usable as a member of another struct. To add it as a possible member type, you need to call ::add_type(name, klass). This will not register it as a constant under CStruct.

If a block is given, a CStruct::Builder is allocated and the block is instance_exec’d for that builder. Encoding strings may not be passed if you opt to use a builder block in place of an encoding string.

### Encodings

Encodings are how you define C structs using Snow::CStruct. It’s a fairly simple string format, defined as such:

length        ::=   '[' integer ']'
alignment     ::=   ':' integer
typename      ::=   ':' Name
member_name   ::=   Name
member_decl   ::=   member_name typename [ length ] [ alignment ]

So, for example, the encoding string “foo: float:8” defines a C struct with a single member, foo, which is an array of 4 32-bit floats with an alignment of 8 bytes. By default, all types are aligned to their base type’s size (e.g., “foo: float” would be algiend to 4 bytes) and all members have a length of 1 unless specified otherwise.

A list of all types follows, including their short and long names, and their corresponding types in C. Short names are only provided for convenience and are generally not too useful except for reducing string length. They’re expanded to their long-form names when the class is created.

  • ‘c / char => char`

  • ‘sc / signed_char => signed char`

  • ‘uc / unsigned_char => unsigned char`

  • ‘ui8 / uint8_t => uint8_t`

  • ‘i8 / int8_t => int8_t`

  • ‘s / short => short`

  • ‘us / unsigned_short => unsigned short`

  • ‘ui16 / uint16_t => uint16_t`

  • ‘i16 / int16_t => int16_t`

  • ‘i32 / int32_t => int32_t`

  • ‘ui32 / uint32_t => uint32_t`

  • ‘ui64 / uint64_t => uint64_t`

  • ‘i64 / int64_t => int64_t`

  • ‘ul / unsigned_long => unsigned long`

  • ‘ull / unsigned_long_long => unsigned long long`

  • ‘l / long => long`

  • ‘ll / long_long => long long`

  • ‘i / int => int`

  • ‘ui / unsigned_int => unsigned int`

  • ‘f / float => float`

  • ‘d / double => double`

  • ‘zu / size_t => size_t`

  • ‘td / ptrdiff_t => ptrdiff_t`

  • ‘ip / intptr_t => intptr_t`

  • ‘uip / uintptr_t => uintptr_t`

  • ‘* / intptr_t => void *` (stored as an intptr_t)

In addition, any structs created with a name or added with #add_type are also valid typenames. So, if a struct with the name :Foo is created, then you can then use it in an encoding, like “bar: Foo [8]” to declare a member that is 8 Foo structs long.

Structs, by default, are aligned to their largest member alignemnt. So, if a struct has four members with alignments of 8, 16, 32, and 4, the struct’s overall alignment is 32 bytes.

Endianness is not handled by structs and must be checked for and handled in your code.

### Struct Classes

Struct classes all declare methods for reading and writing their members. Member access is provided via ‘#get_<member_name>(index = 0)` and modifying members is through `#set_<member_name>(value, index = 0)`. These are also aliased as `#<member_name>` and `#<member_name>=` for convenience, particularly with members whose lengths are 1.

Struct members will always return a new instance of the member type that wraps the member at its address – a copy of the memory at that location is not created.

All struct classes also have an Array class (as StructKlass::Array) as well that provides simple access to resizable arrays. Tehse provide both ‘fetch(index)` and `store(index, value)` methods, both aliased to [] and []= respectively.



409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/snow-data/c_struct.rb', line 409

def self.new(*args, &block)
  klass_name = nil
  encoding = nil

  case
  when args.length == 0 && block_given?
    ; #nop
  when args.length == 1 && block_given?
    klass_name = args[0]
  when args.length == 1
    encoding = args[0]
  when args.length == 2 && !block_given?
    klass_name, encoding = *args
  else
    raise ArgumentError, "wrong number of arguments (#{args.length} for 0..2)"
  end

  klass_name = klass_name.intern if klass_name

  members = if block_given?
    Builder.new(&block).member_info
  else
    decode_member_info(encoding)
  end

  raise "No valid members found in encoding" if members.empty?

  klass = build_struct_type(members)

  if klass_name
    const_set(klass_name, klass)
    add_type(klass_name, klass)
  end

  klass
end

.power_of_two?(num) ⇒ Boolean

call-seq:

power_of_two?(num) => boolean

Returns whether num is a power of two and nonzero.

Returns:

  • (Boolean)


64
65
66
# File 'lib/snow-data/c_struct.rb', line 64

def self.power_of_two?(num)
  ((num & (num - 1)) == 0) && (num != 0)
end

.real_type_of(type) ⇒ Object

Gets the actual type for a given type. Only useful for deducing the target type for a type alias.



217
218
219
220
221
222
# File 'lib/snow-data/c_struct.rb', line 217

def self.real_type_of(type)
  while TYPE_ALIASES.include?(type)
    type = TYPE_ALIASES[type]
  end
  type
end