diff --git a/README.md b/README.md index 29c21e55..3026c26c 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,57 @@ floor = Fiddle::Function.new( puts floor.call(3.14159) #=> 3.0 ``` +### Nested Structs + +You can use hashes to create nested structs, where the hash keys are member names and the values are the nested structs: + +```ruby +StudentCollegeDetail = struct [ + 'int college_id', + 'char college_name[50]' +] + +StudentDetail = struct [ + 'int id', + 'char name[20]', + { clg_data: StudentCollegeDetail } +] +``` + +You can also specify an anonymous nested struct, like so: + +```ruby +StudentDetail = struct [ + 'int id', + 'char name[20]', + { + clg_data: struct([ + 'int college_id', + 'char college_name[50]' + ]) + } +] +``` + +The position of a hash (and the order of the keys in the hash, in the case of a hash with multiple entries), dictate the offsets of the nested struct in memory. The following examples are both syntactically valid but will lay out the structs differently in memory: + +```ruby +# order of members in memory: position, id, dimensions +Rect = struct [ { position: struct(['float x', 'float y']) }, + 'int id', + { dimensions: struct(['float w', 'float h']) } + ] + +# order of members in memory: id, position, dimensions +Rect = struct [ 'int id', + { + position: struct(['float x', 'float y']), + dimensions: struct(['float w', 'float h']) + } + ] +``` + + ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. diff --git a/lib/fiddle/cparser.rb b/lib/fiddle/cparser.rb index fdab7cc2..25ffe25d 100644 --- a/lib/fiddle/cparser.rb +++ b/lib/fiddle/cparser.rb @@ -35,12 +35,37 @@ module CParser def parse_struct_signature(signature, tymap=nil) if signature.is_a?(String) signature = split_arguments(signature, /[,;]/) + elsif signature.is_a?(Hash) + signature = [signature] end mems = [] tys = [] signature.each{|msig| - msig = compact(msig) + msig = compact(msig) if msig.is_a?(String) case msig + when Hash + msig.each do |struct_name, struct_signature| + struct_name = struct_name.to_s if struct_name.is_a?(Symbol) + struct_name = compact(struct_name) + struct_count = nil + if struct_name =~ /^([\w\*\s]+)\[(\d+)\]$/ + struct_count = $2.to_i + struct_name = $1 + end + if struct_signature.respond_to?(:entity_class) + struct_type = struct_signature + else + parsed_struct = parse_struct_signature(struct_signature, tymap) + struct_type = CStructBuilder.create(CStruct, *parsed_struct) + end + if struct_count + ty = [struct_type, struct_count] + else + ty = struct_type + end + mems.push([struct_name, struct_type.members]) + tys.push(ty) + end when /^[\w\*\s]+[\*\s](\w+)$/ mems.push($1) tys.push(parse_ctype(msig, tymap)) diff --git a/lib/fiddle/struct.rb b/lib/fiddle/struct.rb index 318e8314..a766eba8 100644 --- a/lib/fiddle/struct.rb +++ b/lib/fiddle/struct.rb @@ -6,10 +6,67 @@ module Fiddle # A base class for objects representing a C structure class CStruct + include Enumerable + # accessor to Fiddle::CStructEntity def CStruct.entity_class CStructEntity end + + def each + return enum_for(__function__) unless block_given? + + self.class.members.each do |name,| + yield(self[name]) + end + end + + def each_pair + return enum_for(__function__) unless block_given? + + self.class.members.each do |name,| + yield(name, self[name]) + end + end + + def to_h + hash = {} + each_pair do |name, value| + hash[name] = unstruct(value) + end + hash + end + + def replace(another) + if another.nil? + self.class.members.each do |name,| + self[name] = nil + end + elsif another.respond_to?(:each_pair) + another.each_pair do |name, value| + self[name] = value + end + else + another.each do |name, value| + self[name] = value + end + end + self + end + + private + def unstruct(value) + case value + when CStruct + value.to_h + when Array + value.collect do |v| + unstruct(v) + end + else + value + end + end end # A base class for objects representing a C union @@ -27,10 +84,14 @@ class StructArray < Array def initialize(ptr, type, initial_values) @ptr = ptr @type = type - @align = PackInfo::ALIGN_MAP[type] - @size = Fiddle::PackInfo::SIZE_MAP[type] - @pack_format = Fiddle::PackInfo::PACK_MAP[type] - super(initial_values.collect { |v| unsigned_value(v, type) }) + @is_struct = @type.respond_to?(:entity_class) + if @is_struct + super(initial_values) + else + @size = Fiddle::PackInfo::SIZE_MAP[type] + @pack_format = Fiddle::PackInfo::PACK_MAP[type] + super(initial_values.collect { |v| unsigned_value(v, type) }) + end end def to_ptr @@ -42,8 +103,12 @@ def []=(index, value) raise IndexError, 'index %d outside of array bounds 0...%d' % [index, size] end - to_ptr[index * @size, @size] = [value].pack(@pack_format) - super(index, value) + if @is_struct + self[index].replace(value) + else + to_ptr[index * @size, @size] = [value].pack(@pack_format) + super(index, value) + end end end @@ -105,16 +170,23 @@ def create(klass, types, members) define_method(:[]=) { |*args| @entity.send(:[]=, *args) } define_method(:to_ptr){ @entity } define_method(:to_i){ @entity.to_i } + define_singleton_method(:types) { types } + define_singleton_method(:members) { members } members.each{|name| + name = name[0] if name.is_a?(Array) # name is a nested struct + next if method_defined?(name) define_method(name){ @entity[name] } define_method(name + "="){|val| @entity[name] = val } } - size = klass.entity_class.size(types) + entity_class = klass.entity_class + alignment = entity_class.alignment(types) + size = entity_class.size(types) + define_singleton_method(:alignment) { alignment } define_singleton_method(:size) { size } - define_singleton_method(:malloc) do |func=nil| - if block_given? + define_singleton_method(:malloc) do |func=nil, &block| + if block entity_class.malloc(types, func, size) do |entity| - yield new(entity) + block.call(new(entity)) end else new(entity_class.malloc(types, func, size)) @@ -131,6 +203,19 @@ class CStructEntity < Fiddle::Pointer include PackInfo include ValueUtil + def CStructEntity.alignment(types) + max = 1 + types.each do |type, count = 1| + if type.respond_to?(:entity_class) + n = type.alignment + else + n = ALIGN_MAP[type] + end + max = n if n > max + end + max + end + # Allocates a C struct with the +types+ provided. # # See Fiddle::Pointer.malloc for memory management issues. @@ -160,9 +245,15 @@ def CStructEntity.size(types) max_align = types.map { |type, count = 1| last_offset = offset - align = PackInfo::ALIGN_MAP[type] + if type.respond_to?(:entity_class) + align = type.alignment + type_size = type.size + else + align = PackInfo::ALIGN_MAP[type] + type_size = PackInfo::SIZE_MAP[type] + end offset = PackInfo.align(last_offset, align) + - (PackInfo::SIZE_MAP[type] * count) + (type_size * count) align }.max @@ -185,7 +276,28 @@ def initialize(addr, types, func = nil) # Set the names of the +members+ in this C struct def assign_names(members) - @members = members + @members = [] + @nested_structs = {} + members.each_with_index do |member, index| + if member.is_a?(Array) # nested struct + member_name = member[0] + struct_type, struct_count = @ctypes[index] + if struct_count.nil? + struct = struct_type.new(to_i + @offset[index]) + else + structs = struct_count.times.map do |i| + struct_type.new(to_i + @offset[index] + i * struct_type.size) + end + struct = StructArray.new(to_i + @offset[index], + struct_type, + structs) + end + @nested_structs[member_name] = struct + else + member_name = member + end + @members << member_name + end end # Calculates the offsets and sizes for the given +types+ in the struct. @@ -196,12 +308,18 @@ def set_ctypes(types) max_align = types.map { |type, count = 1| orig_offset = offset - align = ALIGN_MAP[type] + if type.respond_to?(:entity_class) + align = type.alignment + type_size = type.size + else + align = ALIGN_MAP[type] + type_size = SIZE_MAP[type] + end offset = PackInfo.align(orig_offset, align) @offset << offset - offset += (SIZE_MAP[type] * count) + offset += (type_size * count) align }.max @@ -230,7 +348,13 @@ def [](*args) end ty = @ctypes[idx] if( ty.is_a?(Array) ) - r = super(@offset[idx], SIZE_MAP[ty[0]] * ty[1]) + if ty.first.respond_to?(:entity_class) + return @nested_structs[name] + else + r = super(@offset[idx], SIZE_MAP[ty[0]] * ty[1]) + end + elsif ty.respond_to?(:entity_class) + return @nested_structs[name] else r = super(@offset[idx], SIZE_MAP[ty.abs]) end @@ -270,6 +394,24 @@ def [](*args) def []=(*args) return super(*args) if args.size > 2 name, val = *args + name = name.to_s if name.is_a?(Symbol) + nested_struct = @nested_structs[name] + if nested_struct + if nested_struct.is_a?(StructArray) + if val.nil? + nested_struct.each do |s| + s.replace(nil) + end + else + val.each_with_index do |v, i| + nested_struct[i] = v + end + end + else + nested_struct.replace(val) + end + return val + end idx = @members.index(name) if( idx.nil? ) raise(ArgumentError, "no such member: #{name}") @@ -307,7 +449,11 @@ class CUnionEntity < CStructEntity # Fiddle::TYPE_VOIDP ]) #=> 8 def CUnionEntity.size(types) types.map { |type, count = 1| - PackInfo::SIZE_MAP[type] * count + if type.respond_to?(:entity_class) + type.size * count + else + PackInfo::SIZE_MAP[type] * count + end }.max end diff --git a/test/fiddle/test_cparser.rb b/test/fiddle/test_cparser.rb index 1590a573..ef8cec5d 100644 --- a/test/fiddle/test_cparser.rb +++ b/test/fiddle/test_cparser.rb @@ -2,6 +2,7 @@ begin require_relative 'helper' require 'fiddle/cparser' + require 'fiddle/import' rescue LoadError end @@ -68,6 +69,19 @@ def test_undefined_ctype_with_type_alias assert_equal(-TYPE_LONG, parse_ctype('DWORD', {"DWORD" => "unsigned long"})) end + def expand_struct_types(types) + types.collect do |type| + case type + when Class + [expand_struct_types(type.types)] + when Array + [expand_struct_types([type[0]])[0][0], type[1]] + else + type + end + end + end + def test_struct_basic assert_equal [[TYPE_INT, TYPE_CHAR], ['i', 'c']], parse_struct_signature(['int i', 'char c']) end @@ -76,6 +90,93 @@ def test_struct_array assert_equal [[[TYPE_CHAR,80],[TYPE_INT,5]], ['buffer','x']], parse_struct_signature(['char buffer[80]', 'int[5] x']) end + def test_struct_nested_struct + types, members = parse_struct_signature([ + 'int x', + {inner: ['int i', 'char c']}, + ]) + assert_equal([[TYPE_INT, [[TYPE_INT, TYPE_CHAR]]], + ['x', ['inner', ['i', 'c']]]], + [expand_struct_types(types), + members]) + end + + def test_struct_nested_defined_struct + inner = Fiddle::Importer.struct(['int i', 'char c']) + assert_equal([[TYPE_INT, inner], + ['x', ['inner', ['i', 'c']]]], + parse_struct_signature([ + 'int x', + {inner: inner}, + ])) + end + + def test_struct_double_nested_struct + types, members = parse_struct_signature([ + 'int x', + { + outer: [ + 'int y', + {inner: ['int i', 'char c']}, + ], + }, + ]) + assert_equal([[TYPE_INT, [[TYPE_INT, [[TYPE_INT, TYPE_CHAR]]]]], + ['x', ['outer', ['y', ['inner', ['i', 'c']]]]]], + [expand_struct_types(types), + members]) + end + + def test_struct_nested_struct_array + types, members = parse_struct_signature([ + 'int x', + { + 'inner[2]' => [ + 'int i', + 'char c', + ], + }, + ]) + assert_equal([[TYPE_INT, [[TYPE_INT, TYPE_CHAR], 2]], + ['x', ['inner', ['i', 'c']]]], + [expand_struct_types(types), + members]) + end + + def test_struct_double_nested_struct_inner_array + types, members = parse_struct_signature(outer: [ + 'int x', + { + 'inner[2]' => [ + 'int i', + 'char c', + ], + }, + ]) + assert_equal([[[[TYPE_INT, [[TYPE_INT, TYPE_CHAR], 2]]]], + [['outer', ['x', ['inner', ['i', 'c']]]]]], + [expand_struct_types(types), + members]) + end + + def test_struct_double_nested_struct_outer_array + types, members = parse_struct_signature([ + 'int x', + { + 'outer[2]' => { + inner: [ + 'int i', + 'char c', + ], + }, + }, + ]) + assert_equal([[TYPE_INT, [[[[TYPE_INT, TYPE_CHAR]]], 2]], + ['x', ['outer', [['inner', ['i', 'c']]]]]], + [expand_struct_types(types), + members]) + end + def test_struct_array_str assert_equal [[[TYPE_CHAR,80],[TYPE_INT,5]], ['buffer','x']], parse_struct_signature('char buffer[80], int[5] x') end diff --git a/test/fiddle/test_import.rb b/test/fiddle/test_import.rb index 88087048..61074506 100644 --- a/test/fiddle/test_import.rb +++ b/test/fiddle/test_import.rb @@ -36,6 +36,29 @@ module LIBC "char c", "unsigned char buff[7]", ] + StructNestedStruct = struct [ + { + "vertices[2]" => { + position: ["float x", "float y", "float z"], + texcoord: ["float u", "float v"] + }, + object: ["int id", "void *user_data"], + }, + "int id" + ] + UnionNestedStruct = union [ + { + keyboard: [ + 'unsigned int state', + 'char key' + ], + mouse: [ + 'unsigned int button', + 'unsigned short x', + 'unsigned short y' + ] + } + ] CallCallback = bind("void call_callback(void*, void*)"){ | ptr1, ptr2| f = Function.new(ptr1.to_i, [TYPE_VOIDP], TYPE_VOID) @@ -93,6 +116,7 @@ def test_sizeof() assert_equal(LIBC::MyStruct.size(), LIBC.sizeof(my_struct)) end assert_equal(SIZEOF_LONG_LONG, LIBC.sizeof("long long")) if defined?(SIZEOF_LONG_LONG) + assert_equal(LIBC::StructNestedStruct.size(), LIBC.sizeof(LIBC::StructNestedStruct)) end Fiddle.constants.grep(/\ATYPE_(?!VOID|VARIADIC\z)(.*)/) do @@ -157,6 +181,257 @@ def test_struct_array_assignment() end end + def test_nested_struct_reusing_other_structs() + position_struct = Fiddle::Importer.struct(['float x', 'float y', 'float z']) + texcoord_struct = Fiddle::Importer.struct(['float u', 'float v']) + vertex_struct = Fiddle::Importer.struct(position: position_struct, texcoord: texcoord_struct) + mesh_struct = Fiddle::Importer.struct([ + { + "vertices[2]" => vertex_struct, + object: [ + "int id", + "void *user_data", + ], + }, + "int id", + ]) + assert_equal LIBC::StructNestedStruct.size, mesh_struct.size + + + keyboard_event_struct = Fiddle::Importer.struct(['unsigned int state', 'char key']) + mouse_event_struct = Fiddle::Importer.struct(['unsigned int button', 'unsigned short x', 'unsigned short y']) + event_union = Fiddle::Importer.union([{ keboard: keyboard_event_struct, mouse: mouse_event_struct}]) + assert_equal LIBC::UnionNestedStruct.size, event_union.size + end + + def test_nested_struct_alignment_is_not_its_size() + inner = Fiddle::Importer.struct(['int x', 'int y', 'int z', 'int w']) + outer = Fiddle::Importer.struct(['char a', { 'nested' => inner }, 'char b']) + outer.malloc(Fiddle::RUBY_FREE) do |instance| + offset = instance.to_ptr.instance_variable_get(:"@offset") + assert_equal Fiddle::SIZEOF_INT * 5, offset.last + assert_equal Fiddle::SIZEOF_INT * 6, outer.size + assert_equal instance.to_ptr.size, outer.size + end + end + + def test_struct_nested_struct_members() + LIBC::StructNestedStruct.malloc(Fiddle::RUBY_FREE) do |s| + Fiddle::Pointer.malloc(24, Fiddle::RUBY_FREE) do |user_data| + s.vertices[0].position.x = 1 + s.vertices[0].position.y = 2 + s.vertices[0].position.z = 3 + s.vertices[0].texcoord.u = 4 + s.vertices[0].texcoord.v = 5 + s.vertices[1].position.x = 6 + s.vertices[1].position.y = 7 + s.vertices[1].position.z = 8 + s.vertices[1].texcoord.u = 9 + s.vertices[1].texcoord.v = 10 + s.object.id = 100 + s.object.user_data = user_data + s.id = 101 + assert_equal({ + "vertices" => [ + { + "position" => { + "x" => 1, + "y" => 2, + "z" => 3, + }, + "texcoord" => { + "u" => 4, + "v" => 5, + }, + }, + { + "position" => { + "x" => 6, + "y" => 7, + "z" => 8, + }, + "texcoord" => { + "u" => 9, + "v" => 10, + }, + }, + ], + "object" => { + "id" => 100, + "user_data" => user_data, + }, + "id" => 101, + }, + s.to_h) + end + end + end + + def test_union_nested_struct_members() + LIBC::UnionNestedStruct.malloc(Fiddle::RUBY_FREE) do |s| + s.keyboard.state = 100 + s.keyboard.key = 101 + assert_equal(100, s.mouse.button) + refute_equal( 0, s.mouse.x) + end + end + + def test_struct_nested_struct_replace_array_element() + LIBC::StructNestedStruct.malloc(Fiddle::RUBY_FREE) do |s| + s.vertices[0].position.x = 5 + + vertex_struct = Fiddle::Importer.struct [{ + position: ["float x", "float y", "float z"], + texcoord: ["float u", "float v"] + }] + vertex_struct.malloc(Fiddle::RUBY_FREE) do |vertex| + vertex.position.x = 100 + s.vertices[0] = vertex + + # make sure element was copied by value, but things like memory address + # should not be changed + assert_equal(100, s.vertices[0].position.x) + refute_equal(vertex.object_id, s.vertices[0].object_id) + refute_equal(vertex.to_ptr, s.vertices[0].to_ptr) + end + end + end + + def test_struct_nested_struct_replace_array_element_nil() + LIBC::StructNestedStruct.malloc(Fiddle::RUBY_FREE) do |s| + s.vertices[0].position.x = 5 + s.vertices[0] = nil + assert_equal({ + "position" => { + "x" => 0.0, + "y" => 0.0, + "z" => 0.0, + }, + "texcoord" => { + "u" => 0.0, + "v" => 0.0, + }, + }, + s.vertices[0].to_h) + end + end + + def test_struct_nested_struct_replace_array_element_hash() + LIBC::StructNestedStruct.malloc(Fiddle::RUBY_FREE) do |s| + s.vertices[0] = { + position: { + x: 10, + y: 100, + } + } + assert_equal({ + "position" => { + "x" => 10.0, + "y" => 100.0, + "z" => 0.0, + }, + "texcoord" => { + "u" => 0.0, + "v" => 0.0, + }, + }, + s.vertices[0].to_h) + end + end + + def test_struct_nested_struct_replace_entire_array() + LIBC::StructNestedStruct.malloc(Fiddle::RUBY_FREE) do |s| + vertex_struct = Fiddle::Importer.struct [{ + position: ["float x", "float y", "float z"], + texcoord: ["float u", "float v"] + }] + + vertex_struct.malloc(Fiddle::RUBY_FREE) do |same0| + vertex_struct.malloc(Fiddle::RUBY_FREE) do |same1| + same = [same0, same1] + same[0].position.x = 1; same[1].position.x = 6 + same[0].position.y = 2; same[1].position.y = 7 + same[0].position.z = 3; same[1].position.z = 8 + same[0].texcoord.u = 4; same[1].texcoord.u = 9 + same[0].texcoord.v = 5; same[1].texcoord.v = 10 + s.vertices = same + assert_equal([ + { + "position" => { + "x" => 1.0, + "y" => 2.0, + "z" => 3.0, + }, + "texcoord" => { + "u" => 4.0, + "v" => 5.0, + }, + }, + { + "position" => { + "x" => 6.0, + "y" => 7.0, + "z" => 8.0, + }, + "texcoord" => { + "u" => 9.0, + "v" => 10.0, + }, + } + ], + s.vertices.collect(&:to_h)) + end + end + end + end + + def test_struct_nested_struct_replace_entire_array_with_different_struct() + LIBC::StructNestedStruct.malloc(Fiddle::RUBY_FREE) do |s| + different_struct_same_size = Fiddle::Importer.struct [{ + a: ['float i', 'float j', 'float k'], + b: ['float l', 'float m'] + }] + + different_struct_same_size.malloc(Fiddle::RUBY_FREE) do |different0| + different_struct_same_size.malloc(Fiddle::RUBY_FREE) do |different1| + different = [different0, different1] + different[0].a.i = 11; different[1].a.i = 16 + different[0].a.j = 12; different[1].a.j = 17 + different[0].a.k = 13; different[1].a.k = 18 + different[0].b.l = 14; different[1].b.l = 19 + different[0].b.m = 15; different[1].b.m = 20 + s.vertices[0][0, s.vertices[0].class.size] = different[0].to_ptr + s.vertices[1][0, s.vertices[1].class.size] = different[1].to_ptr + assert_equal([ + { + "position" => { + "x" => 11.0, + "y" => 12.0, + "z" => 13.0, + }, + "texcoord" => { + "u" => 14.0, + "v" => 15.0, + }, + }, + { + "position" => { + "x" => 16.0, + "y" => 17.0, + "z" => 18.0, + }, + "texcoord" => { + "u" => 19.0, + "v" => 20.0, + }, + } + ], + s.vertices.collect(&:to_h)) + end + end + end + end + def test_struct() LIBC::MyStruct.malloc(Fiddle::RUBY_FREE) do |s| s.num = [0,1,2,3,4]