Class: CTypes::Struct

Inherits:
Object
  • Object
show all
Extended by:
Type
Defined in:
lib/ctypes/struct.rb

Overview

Note:

fields are not automatically aligned based on size; if there are gaps present between c struct fields, you'll need to manually add padding in the layout to reflect that alignment.

This class is used to represent c structures in ruby. It provides methods for converting structs between their byte representation and a ruby representation that can be modified.

Examples:

working with a Type-Length-Value (TLV) struct

# encoding: ASCII-8BIT

# subclass Struct to define a structure
class TLV < CTypes::Struct
  # define structure layout
  layout do
    attribute :type, enum(uint8, %i[hello read write bye])
    attribute :len, uint16.with_endian(:big)
    attribute :value, string
    size { |struct| offsetof(:value) + struct[:len] }
  end

  # add any class or instance methods if needed
end

# pack the struct into bytes
bytes = TLV.pack({type: :hello, len: 5, value: "world"})
                      # => "\0\0\5world"

# unpack bytes into a struct instance
t = TLV.unpack("\0\0\5world")
                      # => #<TLV type=:hello len=5 value="world">

# access struct fields
t.value               # => "world"

# update struct fields, then convert back into bytes
t.type = :bye
t.value = "goodbye"
t.len = t.value.size
t.to_binstr           # => "\3\0\7goodbye"

nested structs

class Attribute < CTypes::Struct
  layout do
    attribute :base, uint8
    attribute :mod, int8
  end
end
class Character < CTypes::Struct
  layout do
    attribute :str, Attribute
    attribute :int, Attribute
    attribute :wis, Attribute
    attribute :dex, Attribute
    attribute :con, Attribute
  end
end

ch = Character.new
ch.str.base = 18
ch.int.base = 8
ch.wis.base = 3
ch.dex.base = 13
ch.con.base = 16
ch.to_binstr        # => "\x12\x00\x08\x00\x03\x00\x0d\x00\x10\x00"
ch.str.mod -= 3
ch.to_binstr        # => "\x12\xFD\x08\x00\x03\x00\x0d\x00\x10\x00"

Defined Under Namespace

Classes: Builder

Instance Attribute Summary

Attributes included from Type

#dry_type, #endian

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Type

default_endian, default_value, fixed_size?, greedy?, pack, pread, read, unpack, unpack_all, unpack_one, with_endian, without_endian

Class Method Details

.==(other) ⇒ Object

Note:

this method does not handle dynamic sized Structs correctly, but the current implementation is sufficient for testing

check if another Struct subclass has the same attributes as this Struct



392
393
394
395
396
397
398
# File 'lib/ctypes/struct.rb', line 392

def self.==(other)
  return true if super
  return false unless other.is_a?(Class) && other < Struct
  other.field_layout == @fields &&
    other.default_endian == default_endian &&
    other.size == size
end

._newStruct

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.

allocate an uninitialized instance of the struct

Returns:

  • (Struct)

    uninitialized struct instance



377
# File 'lib/ctypes/struct.rb', line 377

alias_method :_new, :new

.builderObject



80
81
82
# File 'lib/ctypes/struct.rb', line 80

def self.builder
  Builder.new
end

.export_type(q) ⇒ Object



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/ctypes/struct.rb', line 341

def self.export_type(q)
  q << "CTypes::Struct.builder()"
  q.break
  q.nest(2) do
    q << ".name(%p)\n" % [@name] if @name
    q << ".endian(%p)\n" % [@endian] if @endian
    @fields.each do |name, type|
      case name
      when Symbol
        q << ".attribute(%p, " % [name]
        q << type
        q << ")"
        q.break
      when ::Array
        q << ".attribute("
        q << type
        q << ")"
        q.break
      when Pad
        q << type
        q.break
      else
        raise Error, "unsupported field name type: %p" % [name]
      end
    end
    q << ".build()"
  end
end

.field_layoutObject

return the list of fields with their associated types



310
311
312
# File 'lib/ctypes/struct.rb', line 310

def self.field_layout
  @fields
end

.fieldsObject

return the list of fields in this structure



304
305
306
# File 'lib/ctypes/struct.rb', line 304

def self.fields
  @field_accessors.keys
end

.greedy?Boolean

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.

check if this type is greedy

Returns:

  • (Boolean)


279
280
281
# File 'lib/ctypes/struct.rb', line 279

def self.greedy?
  @greedy
end

.has_field?(k) ⇒ Boolean

check if the struct has a given attribute

Parameters:

  • k (Symbol)

    attribute name

Returns:

  • (Boolean)


298
299
300
# File 'lib/ctypes/struct.rb', line 298

def self.has_field?(k)
  @field_accessors.has_key?(k)
end

.layout(&block) ⇒ Object

define the layout for this structure

Examples:

type-length-value (TLV) struct

class TLV < CTypes::Struct
  layout do
    attribute :type, uint16
    attribute :len, uint16
    attribute :value, string
    size { |s| offsetof(:len) + s.len }
  end
end

Raises:

See Also:



96
97
98
99
100
101
# File 'lib/ctypes/struct.rb', line 96

def self.layout(&block)
  raise Error, "no block given" unless block
  builder = Builder.new
  builder.instance_eval(&block)
  apply_layout(builder)
end

.new(fields = nil) ⇒ Struct

allocate an instance of the Struct and initialize default values

Parameters:

  • fields (Hash) (defaults to: nil)

    values to set

Returns:



384
385
386
387
# File 'lib/ctypes/struct.rb', line 384

def self.new(fields = nil)
  buf = fields.nil? ? ("\0" * size) : pack(fields)
  unpack(buf)
end

.offsetof(attr) ⇒ Integer

get the offset of a field within the structure in bytes

Parameters:

  • attr (Symbol)

    name of the attribute

Returns:

  • (Integer)

    byte offset



267
268
269
270
271
272
273
274
# File 'lib/ctypes/struct.rb', line 267

def self.offsetof(attr)
  @offsets ||= @fields.inject([0, {}]) do |(offset, o), (key, type)|
    o[key] = offset
    [type.size ? offset + type.size : nil, o]
  end.last

  @offsets[attr]
end

.pack(value, endian: default_endian, validate: true) ⇒ ::String

encode a ruby Hash into a String containing the binary representation of the c type

Examples:

pack with default values

include CTypes::Helpers
t = struct(id: uint32, value: uint32)
t.pack({})  # => "\0\0\0\0\0\0\0\0"

pack with some fields

include CTypes::Helpers
t = struct(id: uint32, value: uint32)
t.pack({value: 0xfefefefe})  # => "\x00\x00\x00\x00\xfe\xfe\xfe\xfe"

pack with all fields

include CTypes::Helpers
t = struct(id: uint32, value: uint32)
t.pack({id: 1, value: 2})  # => "\1\0\0\0\2\0\0\0"

pack with nested struct

include CTypes::Helpers
t = struct do
  attribute :id, uint32
  attribute :a, struct(base: uint8, mod: uint8)
end
t.pack({id: 1, a: {base: 2, mod: 3}}) # => "\1\0\0\0\2\3"

Parameters:

  • value (Hash)

    value to be encoded

  • endian (Symbol) (defaults to: default_endian)

    endian to pack with

  • validate (Boolean) (defaults to: true)

    set to false to disable value validation

Returns:

  • (::String)

    binary encoding for value



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/ctypes/struct.rb', line 164

def self.pack(value, endian: default_endian, validate: true)
  value = value.to_hash.freeze
  value = @dry_type[value] unless validate == false
  buf = ::String.new
  @fields.each do |(name, type)|
    case name
    when Pad
      buf << type.pack(nil)
    when Symbol
      buf << type.pack(value[name],
        endian: type.endian || endian,
        validate: false)
    when ::Array
      buf << type.pack(value.slice(*name),
        endian: type.endian || endian,
        validate: false)
    else
      raise Error, "unsupported field name type: %p" % [name]
    end
  end

  return buf if fixed_size? || @size.nil?

  size = instance_exec(value, &@size)
  if size > buf.size
    buf << "\0" * (size - buf.size)
  elsif size < buf.size
    buf[0, size]
  else
    buf
  end
end

.pretty_print(q) ⇒ Object

:nodoc:



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/ctypes/struct.rb', line 320

def self.pretty_print(q) # :nodoc:
  q.ctype("struct", @endian) do
    q.line("name %p" % [@name]) if @name
    q.seplist(@fields, -> { q.breakable(";") }) do |name, type|
      case name
      when Symbol
        q.text("attribute %p, " % name)
        q.pp(type)
      when ::Array
        q.text("attribute ")
        q.pp(type)
      when Pad
        q.pp(type)
      else
        raise Error, "unsupported field name type: %p" % [name]
      end
    end
  end
end

.sizeInteger

get the minimum size of the structure

For fixed-size structures, this will return the size of the structure. For dynamic length structures, this will return the minimum size of the structure

Returns:

  • (Integer)

    structure size in bytes



290
291
292
293
294
# File 'lib/ctypes/struct.rb', line 290

def self.size
  return @size if @size.is_a?(Integer)

  @min_size ||= @fields&.inject(0) { |s, (_, t)| s + t.size } || 0
end

.type_nameObject

return the struct name if supplied



316
317
318
# File 'lib/ctypes/struct.rb', line 316

def self.type_name
  @name
end

.unpack_one(buf, endian: default_endian) ⇒ ::Array(Struct, ::String)

convert a String containing the binary represention of a c struct into a ruby type

Examples:

class TLV < CTypes::Struct
  layout do
    attribute :type, enum(uint8, %i[hello, read, write, bye])
    attribute :len, uint16.with_endian(:big)
    attribute :value, string
    size { |struct| offsetof(:value) + struct[:len] }
  end
end
TLV.unpack_one("\0\0\5helloextra")
    # => [#<TLV type=:hello len=5 value="hello">, "extra"]

Parameters:

  • buf (String)

    bytes that make up the type

  • endian (Symbol) (defaults to: default_endian)

    endian of data within buf

Returns:

  • (::Array(Struct, ::String))

    decoded struct, and remaining bytes

See Also:



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/ctypes/struct.rb', line 218

def self.unpack_one(buf, endian: default_endian)
  rest = buf
  trimmed = nil # set to the unused portion of buf when we have @size
  out = _new    # output structure instance
  out.instance_variable_set(:@endian, endian)

  @fields.each do |(name, type)|
    # if the type is greedy, and we have a dynamic size, and we haven't
    # already trimmed the input buffer, let's do so now.
    #
    # note: we do this while unpacking because the @size proc may require
    # some of the unpacked fields to determine the size of the struct such
    # as in TLV structs
    if type.greedy? && @size && !trimmed

      # caluclate the total size of the struct from the decoded fields
      size = instance_exec(out, &@size)
      raise missing_bytes_error(input: buf, need: size) if
        size > buf.size

      # adjust the size for how much we've already unpacked
      size -= offsetof(name.is_a?(Array) ? name[0] : name)

      # split the remaining buffer; we stick the part we aren't going to
      # use in trimmed, and update rest to point at our buffer
      trimmed = rest.byteslice(size..)
      rest = rest.byteslice(0, size)
    end

    value, rest = type.unpack_one(rest, endian: type.endian || endian)
    case name
    when Symbol
      out[name] = value
    when ::Array
      name.each { |n| out[n] = value[n] }
    when Pad
      # no op
    else
      raise Error, "unsupported field name type: %p" % [name]
    end
  end

  [out, trimmed || rest]
end

Instance Method Details

#==(other) ⇒ Object

Note:

this implementation also supports Hash equality through #to_h

determine if this instance of the struct is equal to another instance



495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/ctypes/struct.rb', line 495

def ==(other)
  case other
  when self.class
    self.class.field_layout.all? do |field, _|
      instance_variable_get(:"@#{field}") == other[field]
    end
  when Hash
    other == to_h
  else
    super
  end
end

#[](k) ⇒ Object

get an attribute value

Examples:

include CTypes::Helpers
t = struct(id: uint32, value: uint32)
i = t.new
i[:value] = 123
i[:value]                   # => 123

Parameters:

  • k (Symbol)

    attribute name

Returns:

  • value



427
428
429
430
# File 'lib/ctypes/struct.rb', line 427

def [](k)
  has_attribute!(k)
  instance_variable_get(:"@#{k}")
end

#[]=(k, v) ⇒ Object

set an attribute value

Examples:

include CTypes::Helpers
t = struct(id: uint32, value: uint32)
i = t.new
i[:id] = 12
i.id            # => 12
i.id = 55
i.id            # => 55

Parameters:

  • k (Symbol)

    attribute name

  • v

    value



412
413
414
415
# File 'lib/ctypes/struct.rb', line 412

def []=(k, v)
  has_attribute!(k)
  instance_variable_set(:"@#{k}", v)
end

#has_key?(name) ⇒ Boolean

check if the CTypes::Struct has a specific attribute name

Returns:

  • (Boolean)


433
434
435
# File 'lib/ctypes/struct.rb', line 433

def has_key?(name)
  self.class.has_field?(name)
end

#pretty_print(q) ⇒ Object

:nodoc:



508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
# File 'lib/ctypes/struct.rb', line 508

def pretty_print(q) # :nodoc:
  open = if (name = self.class.type_name || self.class.name)
    "struct #{name} {"
  else
    "struct {"
  end
  q.group(4, open, "}") do
    # strip out pad fields
    fields = self.class.field_layout.reject do |(_, type)|
      type.is_a?(CTypes::Pad)
    end
    q.seplist(fields, -> { q.breakable("") }) do |name, _|
      names = name.is_a?(::Array) ? name : [name]
      names.each do |name|
        next if name.is_a?(CTypes::Pad)
        q.text(".#{name} = ")
        q.pp(instance_variable_get(:"@#{name}"))
        q.text(", ")
      end
    end
  end
end

#to_binstr(endian: @endian) ⇒ String

return the binary representation of this Struct instance

Examples:

include CTypes::Helpers
t = struct(id: uint32, value: string)
i = t.new
i.id = 1
i.value = "hello"
i.to_binstr               # => "\1\0\0\0hello"

Returns:

  • (String)

    binary representation of struct



488
489
490
# File 'lib/ctypes/struct.rb', line 488

def to_binstr(endian: @endian)
  self.class.pack(to_h, endian:)
end

#to_h(shallow: false) ⇒ Hash Also known as: to_hash

return a Hash representation of the data type

Examples:

deep

include CTypes::Helpers
t = struct do
  attribute :inner, struct(value: uint8)
end
i = t.new
i.inner.value = 5
i.to_h                      # => {inner: {value: 5}}

shallow

include CTypes::Helpers
t = struct do
  attribute :inner, struct(value: uint8)
end
i = t.new
i.inner.value = 5
i.to_h(shallow: true)       # => {inner: #<Class:0x646456 value=5>}

Parameters:

  • shallow (Boolean) (defaults to: false)

    set to true to disable deep traversal

Returns:

  • (Hash)


465
466
467
468
469
470
471
472
473
474
475
# File 'lib/ctypes/struct.rb', line 465

def to_h(shallow: false)
  out = {}
  self.class.fields.each do |field|
    value = send(field)
    unless shallow || value.is_a?(::Array) || !value.respond_to?(:to_h)
      value = value.to_h
    end
    out[field] = value
  end
  out
end