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
      )?
    (?<offset_decl> \s* @ \s*                     # 6
      (?<offset> \d+ )                            # 7
    )?
  \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

.__build_struct__(name, is_union, &block) ⇒ Object

:nodoc:

Passes a block through to a Builder and provides a flag for whether the root level of the builder is a struct or union.



452
453
454
455
# File 'lib/snow-data/c_struct.rb', line 452

def self.__build_struct__(name, is_union, &block)
  members = Builder.new(is_union: is_union, &block).member_info
  __define_struct__(name, build_struct_type(members))
end

.__define_struct__(name, klass) ⇒ Object

:nodoc:

Used by ::build_struct and ::new to handle defining a struct class’s constant and adding its type to those recognized by CStruct.



522
523
524
525
526
527
528
529
# File 'lib/snow-data/c_struct.rb', line 522

def self.__define_struct__(name, klass)
  if name
    name = name.to_sym
    const_set(name, klass)
    add_type(name, klass)
  end
  klass
end

.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.



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
296
297
298
# File 'lib/snow-data/c_struct.rb', line 255

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



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

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.



638
639
640
641
642
643
644
645
646
# File 'lib/snow-data/c_struct.rb', line 638

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)


577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
# File 'lib/snow-data/c_struct.rb', line 577

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(&:alignment).max
  type_size    = members.map { |m| m.offset + m.size }.max
  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.



539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
# File 'lib/snow-data/c_struct.rb', line 539

def self.decode_member_info(encoding)
  total_size = 0
  encoding.scan(ENCODING_REGEX).map do
    |match|
    name        = match[0].to_sym
    type        = real_type_of(match[1].to_sym)
    length      = (match[3] || 1).to_i
    align       = (match[5] || ALIGNMENTS[type] || 1).to_i
    size        = SIZES[type] * length
    offset      = (match[7] || 0).to_i
    offset += Memory.align_size(total_size, align) if ! match[7]
    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.



563
564
565
566
567
# File 'lib/snow-data/c_struct.rb', line 563

def self.encode_member_info(members)
  members.map { |member|
    "#{member.name}:#{member.type}[#{member.length}]:#{member.alignment}@#{member.offset}"
  }.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.to_sym
  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:

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

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.

Offsets should only be specified if you absolutely know what you’re doing, otherwise you may break certain things (for example, native sizing on ints). In addition, an offset can be provided to simulate union-like behavior for some members, though you are better off using the Builder methods to define a union than you are via an encoding string.

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.



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
# File 'lib/snow-data/c_struct.rb', line 419

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

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

  __define_struct__(klass_name, build_struct_type(members))
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.



220
221
222
223
224
225
# File 'lib/snow-data/c_struct.rb', line 220

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

.struct(name = nil, &block) ⇒ Object

call-seq:

struct { ... } => Class
struct(name) { ... } => Class

Defines a struct with a block for declaring the members of the struct using Builder methods. Members of structs do not share space, unlike unions.

If a name is provided, the resulting class will be added as a constant under CStruct and it will be added as a type recognized in other CStruct-defined structs and unions.

### Example

CStruct.struct {
  # size, red, green, and blue all get their own memory in the struct,
  # one after the other. Modifying one value will not modify another.
  size_t    :size
  uint16_t  :red[256]
  uint16_t  :green[256]
  uint16_t  :blue[256]
}


511
512
513
# File 'lib/snow-data/c_struct.rb', line 511

def self.struct(name = nil, &block)
  __build_struct__(name, false, &block)
end

.union(name = nil, &block) ⇒ Object

call-seq:

union { ... } => Class
union(name) { ... } => Class

Defines a union with a block for declaring the members of the union using Builder methods. Unions are basically structs whose root members occupy the same or adjacent locations in memory. You may use Builder#struct to define multiple internal unnamed structs inside a union.

If a name is provided, the resulting class will be added as a constant under CStruct and it will be added as a type recognized in other CStruct-defined structs and unions.

### Example

CStruct.union {
  # size, name, and some_value all share the same memory in the union
  # so set_name will modify size and some_value, and any transposition
  # of those names is also true.
  size_t    :size
  uint32_t  :name
  double    :some_value
}


483
484
485
# File 'lib/snow-data/c_struct.rb', line 483

def self.union(name = nil, &block)
  __build_struct__(name, true, &block)
end