ffi_dry 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.rdoc +233 -0
- data/Rakefile +60 -0
- data/VERSION +1 -0
- data/ffi_dry.gemspec +60 -0
- data/lib/ffi/dry.rb +366 -0
- data/lib/ffi_dry.rb +1 -0
- data/samples/afmap.rb +28 -0
- data/samples/basic.rb +55 -0
- data/samples/describer.rb +119 -0
- data/spec/ffi_dry_spec.rb +7 -0
- data/spec/spec_helper.rb +9 -0
- metadata +89 -0
data/.document
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Eric Monti
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
= ffi_dry
|
2
|
+
|
3
|
+
Helpers, sugar methods, and new features over Ruby FFI to do some common
|
4
|
+
things and add support for some uncommon ones.
|
5
|
+
|
6
|
+
== Requirements
|
7
|
+
|
8
|
+
* ffi-ffi (>= 0.5.0) - github.com/ffi/ffi
|
9
|
+
|
10
|
+
|
11
|
+
== Synopsis
|
12
|
+
|
13
|
+
(samples/ in the package for code)
|
14
|
+
|
15
|
+
A major feature is a DSL"-like" syntax for declaring structure members
|
16
|
+
in FFI::Struct or FFI::ManagedStruct definitions.
|
17
|
+
|
18
|
+
require 'rubygems'
|
19
|
+
require 'ffi'
|
20
|
+
require 'ffi/dry'
|
21
|
+
|
22
|
+
class SomeStruct < FFI::Struct
|
23
|
+
include FFI::DRY::StructHelper
|
24
|
+
|
25
|
+
# we get a new way of specifying layouts with a 'dsl'-like syntax
|
26
|
+
# The hash containing {:desc => ... } can contain arbitrary keys which
|
27
|
+
# can be used however we like. dsl_metadata will contain all these
|
28
|
+
# in the class and instance.
|
29
|
+
dsl_layout do
|
30
|
+
field :field1, :uint16, :desc => 'this is field 1'
|
31
|
+
field :field2, :uint16, :desc => 'this is field 2'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
ss0=SomeStruct.new
|
36
|
+
|
37
|
+
With the declarations above, we specified :desc hash value in metadata
|
38
|
+
Let's check out in our instance.
|
39
|
+
|
40
|
+
pp ss0.dsl_metadata
|
41
|
+
[{:type=>:uint16, :name=>:field1, :desc=>"this is field 1"},
|
42
|
+
{:type=>:uint16, :name=>:field2, :desc=>"this is field 2"}]
|
43
|
+
# => nil
|
44
|
+
|
45
|
+
Or class.
|
46
|
+
|
47
|
+
pp SomeStruct.dsl_metadata
|
48
|
+
#...
|
49
|
+
|
50
|
+
We get some additional ways of instantiating and declaring values for free
|
51
|
+
during initialization. (The FFI standard ways still work too)
|
52
|
+
|
53
|
+
raw_data = "\x00\x00\xff\xff"
|
54
|
+
|
55
|
+
ss1=SomeStruct.new :raw => raw_data
|
56
|
+
ss2=SomeStruct.new :raw => raw_data, :field1 => 1, :field2 => 2
|
57
|
+
ss3=SomeStruct.new {|x| x.field1=1 }
|
58
|
+
ss4=SomeStruct.new(:raw => raw_data) {|x| x.field1=1 }
|
59
|
+
|
60
|
+
[ ss0,
|
61
|
+
ss1,
|
62
|
+
ss2,
|
63
|
+
ss3,
|
64
|
+
ss4 ].each_with_index {|x,i| p ["ss#{i}",[x.field1, x.field2]]}
|
65
|
+
|
66
|
+
# which will produce...
|
67
|
+
# ["ss0", [0, 0]]
|
68
|
+
# ["ss1", [0, 65535]]
|
69
|
+
# ["ss2", [1, 2]]
|
70
|
+
# ["ss3", [1, 0]]
|
71
|
+
# ["ss4", [1, 65535]]
|
72
|
+
|
73
|
+
|
74
|
+
Here's a broader example which utilizes that arbitrary ':desc' parameter in a
|
75
|
+
"neighborly" way. This also demonstrates superclasses to add common struct
|
76
|
+
features, declaring array fields, as well as nesting other structs.
|
77
|
+
|
78
|
+
require 'rubygems'
|
79
|
+
require 'ffi'
|
80
|
+
require 'ffi/dry'
|
81
|
+
|
82
|
+
class NeighborlyStruct < ::FFI::Struct
|
83
|
+
include ::FFI::DRY::StructHelper
|
84
|
+
|
85
|
+
def self.describe
|
86
|
+
print "Struct: #{self.name}"
|
87
|
+
dsl_metadata().each_with_index do |spec, i|
|
88
|
+
print " Field #{i}\n"
|
89
|
+
print " name: #{spec[:name].inspect}\n"
|
90
|
+
print " type: #{spec[:type].inspect}\n"
|
91
|
+
print " desc: #{spec[:desc]}\n\n"
|
92
|
+
end
|
93
|
+
print "\n"
|
94
|
+
end
|
95
|
+
def describe; self.class.describe; end
|
96
|
+
end
|
97
|
+
|
98
|
+
class TestStruct < NeighborlyStruct
|
99
|
+
dsl_layout do
|
100
|
+
field :field1, :uint8, :desc => "test field 1"
|
101
|
+
field :field2, :uint8, :desc => "test field 2"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class SomeStruct < NeighborlyStruct
|
106
|
+
dsl_layout do
|
107
|
+
field :kind, :uint8, :desc => "a type identifier"
|
108
|
+
struct :tst, TestStruct, :desc => "a nested TestStruct"
|
109
|
+
field :len, :uint8, :desc => "8-bit size value (>= self.size+2)"
|
110
|
+
array :str, [:char,255],
|
111
|
+
:desc => "a string up to 255 bytes bound by :len"
|
112
|
+
end
|
113
|
+
|
114
|
+
# override kind getter method with our own
|
115
|
+
# resolves kind to some kind of type array for example...
|
116
|
+
def kind
|
117
|
+
[:default, :bar, :baz][ self[:kind] ]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
s1=TestStruct.new
|
122
|
+
s2=SomeStruct.new
|
123
|
+
|
124
|
+
# check out that 'kind' override:
|
125
|
+
s2.kind
|
126
|
+
# => :default
|
127
|
+
|
128
|
+
# oh and the regular FFI way is always intact
|
129
|
+
s2[:kind]
|
130
|
+
# => 0
|
131
|
+
|
132
|
+
s2[:kind]=1
|
133
|
+
s2.kind
|
134
|
+
# => :bar
|
135
|
+
|
136
|
+
s2.kind=3
|
137
|
+
s2.kind
|
138
|
+
# => :baz
|
139
|
+
|
140
|
+
puts "*"*70
|
141
|
+
s1.describe
|
142
|
+
## we get a dump of metadata
|
143
|
+
# **********************************************************************
|
144
|
+
# Struct: TestStruct
|
145
|
+
# Field 0
|
146
|
+
# name: :field1
|
147
|
+
# type: :uint8
|
148
|
+
# desc: test field 1
|
149
|
+
#
|
150
|
+
# Field 1
|
151
|
+
# name: :field2
|
152
|
+
# type: :uint8
|
153
|
+
# desc: test field 2
|
154
|
+
|
155
|
+
puts "*"*70
|
156
|
+
s2.describe
|
157
|
+
## we get a dump of metadata
|
158
|
+
# Struct: SomeStruct Field 0
|
159
|
+
# name: :kind
|
160
|
+
# type: :uint8
|
161
|
+
# desc: a type identifier
|
162
|
+
#
|
163
|
+
# Field 1
|
164
|
+
# name: :tst
|
165
|
+
# type: TestStruct
|
166
|
+
# desc: a nested TestStruct
|
167
|
+
#
|
168
|
+
# Field 2
|
169
|
+
# name: :len
|
170
|
+
# type: :uint8
|
171
|
+
# desc: 8-bit size value (>= self.size+2)
|
172
|
+
#
|
173
|
+
# Field 3
|
174
|
+
# name: :str
|
175
|
+
# type: [:char, 255]
|
176
|
+
# desc: a string up to 255 bytes bound by :len
|
177
|
+
|
178
|
+
puts "*"*70
|
179
|
+
s2.tst.describe
|
180
|
+
## same as s1.describe
|
181
|
+
# **********************************************************************
|
182
|
+
# Struct: TestStruct
|
183
|
+
# Field 0
|
184
|
+
# name: :field1
|
185
|
+
# type: :uint8
|
186
|
+
# desc: test field 1
|
187
|
+
#
|
188
|
+
# Field 1
|
189
|
+
# name: :field2
|
190
|
+
# type: :uint8
|
191
|
+
# desc: test field 2
|
192
|
+
|
193
|
+
There's also some helpers for collecting lookup maps for constants, a common
|
194
|
+
and handy thing when porting various libraries. We use Socket here just for
|
195
|
+
example purposes, you can 'slurp' constants form any namespace this way.
|
196
|
+
|
197
|
+
require 'ffi/dry'
|
198
|
+
require 'socket'
|
199
|
+
|
200
|
+
module AddressFamily
|
201
|
+
include FFI::DRY::ConstMap
|
202
|
+
slurp_constants ::Socket, "AF_"
|
203
|
+
def list ; @@list ||= super() ; end # only generate the hash once
|
204
|
+
end
|
205
|
+
|
206
|
+
AddressFamily now has all the constants it found for Socket::AF_* minus the
|
207
|
+
prefix.
|
208
|
+
|
209
|
+
AddressFamily::INET
|
210
|
+
AddressFamily::LINK
|
211
|
+
AddressFamily::INET6
|
212
|
+
|
213
|
+
etc...
|
214
|
+
|
215
|
+
We can do type or value lookups using []
|
216
|
+
|
217
|
+
AddressFamily[2] # => "INET"
|
218
|
+
AddressFamily["INET"] # => 2
|
219
|
+
|
220
|
+
We can get a hash of all constant->value pairs with .list
|
221
|
+
|
222
|
+
AddressFamily.list
|
223
|
+
# => {"NATM"=>31, "DLI"=>13, "UNIX"=>1, "NETBIOS"=>33, ...}
|
224
|
+
|
225
|
+
... and invert for a reverse mapping
|
226
|
+
|
227
|
+
AddressFamily.list.invert
|
228
|
+
# => {16=>"APPLETALK", 5=>"CHAOS", 27=>"NDRV", 0=>"UNSPEC", ...}
|
229
|
+
|
230
|
+
|
231
|
+
== License
|
232
|
+
|
233
|
+
Copyright (c) 2009 Eric Monti. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/clean'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |gem|
|
8
|
+
gem.name = "ffi_dry"
|
9
|
+
gem.summary = %Q{Syntactic sugar and helper utilities for FFI}
|
10
|
+
gem.description = %Q{Provides a some useful modules, classes, and methods as well as a DSL-like syntax for FFI::Struct layouts}
|
11
|
+
gem.email = "emonti@matasano.com"
|
12
|
+
gem.homepage = "http://github.com/emonti/ffi_dry"
|
13
|
+
gem.authors = ["Eric Monti"]
|
14
|
+
# gem.add_dependency "ffi", ">= 0.5.0"
|
15
|
+
gem.add_development_dependency "rspec"
|
16
|
+
gem.add_development_dependency "yard"
|
17
|
+
end
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'spec/rake/spectask'
|
23
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
24
|
+
spec.libs << 'lib' << 'spec'
|
25
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
26
|
+
end
|
27
|
+
|
28
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
31
|
+
spec.rcov = true
|
32
|
+
end
|
33
|
+
|
34
|
+
task :spec => :check_dependencies
|
35
|
+
|
36
|
+
task :default => :spec
|
37
|
+
|
38
|
+
begin
|
39
|
+
require 'yard'
|
40
|
+
YARD::Rake::YardocTask.new
|
41
|
+
rescue LoadError
|
42
|
+
task :yardoc do
|
43
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
require 'rake/rdoctask'
|
48
|
+
Rake::RDocTask.new do |rdoc|
|
49
|
+
if File.exist?('VERSION')
|
50
|
+
version = File.read('VERSION')
|
51
|
+
else
|
52
|
+
version = ""
|
53
|
+
end
|
54
|
+
|
55
|
+
rdoc.rdoc_dir = 'rdoc'
|
56
|
+
rdoc.title = "ffi_dry #{version}"
|
57
|
+
rdoc.rdoc_files.include('README*')
|
58
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
59
|
+
end
|
60
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.3
|
data/ffi_dry.gemspec
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{ffi_dry}
|
8
|
+
s.version = "0.1.3"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Eric Monti"]
|
12
|
+
s.date = %q{2009-09-16}
|
13
|
+
s.description = %q{Provides a some useful modules, classes, and methods as well as a DSL-like syntax for FFI::Struct layouts}
|
14
|
+
s.email = %q{emonti@matasano.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"ffi_dry.gemspec",
|
27
|
+
"lib/ffi/dry.rb",
|
28
|
+
"lib/ffi_dry.rb",
|
29
|
+
"samples/afmap.rb",
|
30
|
+
"samples/basic.rb",
|
31
|
+
"samples/describer.rb",
|
32
|
+
"spec/ffi_dry_spec.rb",
|
33
|
+
"spec/spec_helper.rb"
|
34
|
+
]
|
35
|
+
s.homepage = %q{http://github.com/emonti/ffi_dry}
|
36
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubygems_version = %q{1.3.4}
|
39
|
+
s.summary = %q{Syntactic sugar and helper utilities for FFI}
|
40
|
+
s.test_files = [
|
41
|
+
"spec/ffi_dry_spec.rb",
|
42
|
+
"spec/spec_helper.rb"
|
43
|
+
]
|
44
|
+
|
45
|
+
if s.respond_to? :specification_version then
|
46
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
47
|
+
s.specification_version = 3
|
48
|
+
|
49
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
50
|
+
s.add_development_dependency(%q<rspec>, [">= 0"])
|
51
|
+
s.add_development_dependency(%q<yard>, [">= 0"])
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
54
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
55
|
+
end
|
56
|
+
else
|
57
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
58
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
59
|
+
end
|
60
|
+
end
|
data/lib/ffi/dry.rb
ADDED
@@ -0,0 +1,366 @@
|
|
1
|
+
begin; require 'rubygems'; rescue LoadError; end
|
2
|
+
|
3
|
+
require 'ffi'
|
4
|
+
|
5
|
+
module FFI::DRY
|
6
|
+
|
7
|
+
# A module to add syntactic sugar and some nice automatic getter/setter
|
8
|
+
# logic to FFI::Struct, FFI::ManagedStruct, etc.
|
9
|
+
#
|
10
|
+
# For example:
|
11
|
+
# require 'rubygems'
|
12
|
+
# require 'ffi'
|
13
|
+
# require 'ffi/dry'
|
14
|
+
# require 'pp'
|
15
|
+
#
|
16
|
+
# class SomeStruct < FFI::Struct
|
17
|
+
# include FFI::DRY::StructHelper
|
18
|
+
#
|
19
|
+
# # we get a new way of specifying layouts with a 'dsl'-like syntax
|
20
|
+
# dsl_layout do
|
21
|
+
# field :field1, :uint16, :desc => 'this is field 1'
|
22
|
+
# field :field2, :uint16, :desc => 'this is field 2'
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# ss0=SomeStruct.new
|
27
|
+
#
|
28
|
+
# pp ss0.dsl_metadata # we can look at definition metadata
|
29
|
+
#
|
30
|
+
# # produces...
|
31
|
+
# # [{:type=>:uint16, :name=>:field1, :desc=>"this is field 1"},
|
32
|
+
# # {:type=>:uint16, :name=>:field2, :desc=>"this is field 2"}]
|
33
|
+
#
|
34
|
+
# # And we have additional ways of instantiating and declaring values
|
35
|
+
# # during initialization. (The FFI standard ways still work too)
|
36
|
+
#
|
37
|
+
# raw_data = "\x00\x00\xff\xff"
|
38
|
+
#
|
39
|
+
# ss1=SomeStruct.new :raw => raw_data
|
40
|
+
# ss2=SomeStruct.new :raw => raw_data, :field1 => 1, :field2 => 2
|
41
|
+
# ss3=SomeStruct.new {|x| x.field1=1 }
|
42
|
+
# ss4=SomeStruct.new(:raw => raw_data) {|x| x.field1=1 }
|
43
|
+
#
|
44
|
+
# [ ss0,
|
45
|
+
# ss1,
|
46
|
+
# ss2,
|
47
|
+
# ss3,
|
48
|
+
# ss4].each_with_index {|x,i| pp ["ss#{i}",[x.field1, x.field2]]}
|
49
|
+
#
|
50
|
+
# # produces...
|
51
|
+
# # ["ss0", [0, 0]]
|
52
|
+
# # ["ss1", [0, 65535]]
|
53
|
+
# # ["ss2", [1, 2]]
|
54
|
+
# # ["ss3", [1, 0]]
|
55
|
+
# # ["ss4", [1, 65535]]
|
56
|
+
#
|
57
|
+
module StructHelper #< ::FFI::Struct
|
58
|
+
|
59
|
+
attr_reader :dsl_metadata
|
60
|
+
|
61
|
+
# Adds field setting on initialization to ::FFI::Struct.new as well as
|
62
|
+
# a "yield(self) if block_given?" at the end.
|
63
|
+
#
|
64
|
+
# The field initialization kicks in if there is only one argument, and it
|
65
|
+
# is a Hash.
|
66
|
+
#
|
67
|
+
# Note:
|
68
|
+
# The :raw parameter is a special tag in the hash. The value is taken as a
|
69
|
+
# string and initialized into a new FFI::MemoryPointer which this Struct
|
70
|
+
# then overlays.
|
71
|
+
#
|
72
|
+
# If your struct layout has a field named :raw field, it won't be
|
73
|
+
# assignable through the hash argument.
|
74
|
+
#
|
75
|
+
# See also: set_fields() which is called automatically on the hash, minus
|
76
|
+
# the :raw tag.
|
77
|
+
#
|
78
|
+
def initialize(*args)
|
79
|
+
@dsl_metadata = self.class.dsl_metadata
|
80
|
+
params=nil
|
81
|
+
|
82
|
+
if args.size == 1 and (oparams=args[0]).is_a? Hash
|
83
|
+
params = oparams.dup
|
84
|
+
if raw=params.delete(:raw)
|
85
|
+
super( ::FFI::MemoryPointer.from_string(raw) )
|
86
|
+
else
|
87
|
+
super()
|
88
|
+
end
|
89
|
+
else
|
90
|
+
super(*args)
|
91
|
+
end
|
92
|
+
|
93
|
+
set_fields(params)
|
94
|
+
yield self if block_given?
|
95
|
+
end
|
96
|
+
|
97
|
+
# Sets field values in the struct specified by their symbolic name from a
|
98
|
+
# hash of ':field => value' pairs. Uses accessor field wrapper methods
|
99
|
+
# instead of a direct reference to the field (as in "obj.field1 = x",
|
100
|
+
# not "obj[:field] = x"). The difference is subtle, but this allows you
|
101
|
+
# to take advantage of any wrapper methods you override when initializing
|
102
|
+
# a new object. The only caveat is that the wrapper method must be named
|
103
|
+
# the same as the field, and the field must be included in members() from
|
104
|
+
# the layout.
|
105
|
+
#
|
106
|
+
# This method is called automatically if you are using the initialize()
|
107
|
+
# method provided in the DryStruct class and passing it a Hash as its only
|
108
|
+
# argument.
|
109
|
+
def set_fields(params=nil)
|
110
|
+
(params || {}).keys.each do |p|
|
111
|
+
if members().include?(p)
|
112
|
+
self.__send__(:"#{p}=", params[p])
|
113
|
+
else
|
114
|
+
raise(::ArgumentError, "#{self.class} does not have a '#{p}' field")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns a new instance of self.class containing a seperately allocated
|
120
|
+
# copy of all our data. This abstract method should usually be called
|
121
|
+
# with super() from overridden 'copy' implementations for structures
|
122
|
+
# containing pointers to other memory or variable length data at the end.
|
123
|
+
#
|
124
|
+
# Note also that, by default, this implementation determine's size
|
125
|
+
# automatically based on the structure size. This is comparable to
|
126
|
+
# sizeof(some_struct) in C. However, you can supply a 'grown' parameter
|
127
|
+
# which can be used to add to the size of the copied instance as it is
|
128
|
+
# allocated and copied.
|
129
|
+
def copy(grown=0)
|
130
|
+
self.class.new( :raw => self.to_ptr.read_string(self.size+grown) )
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns a pointer to the specified field, which is the name assigned
|
134
|
+
# to a member in the layout.
|
135
|
+
def ptr_to(field)
|
136
|
+
x = self[field] # this is actually a test, to raise if missing
|
137
|
+
return (self.to_ptr + self.offset_of(field))
|
138
|
+
end
|
139
|
+
|
140
|
+
# Contains dsl_layout and some support methods that an 'includee' of
|
141
|
+
# DryStructHelper will have available as class methods.
|
142
|
+
module ClassMethods
|
143
|
+
# returns the structure metadata for this class based on
|
144
|
+
# the dsl_layout definitions
|
145
|
+
def dsl_metadata
|
146
|
+
@dsl_metadata
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
# This passes a block to an instance of DSL_StructLayoutBuilder, allowing
|
152
|
+
# for a more declarative syntax.
|
153
|
+
#
|
154
|
+
# It is a replacement to layout() and stores the dsl_metadata gathered
|
155
|
+
# about structure members locally.
|
156
|
+
#
|
157
|
+
#
|
158
|
+
def dsl_layout &block
|
159
|
+
builder = DSL_StructLayoutBuilder.new(self)
|
160
|
+
builder.instance_eval(&block)
|
161
|
+
@layout = builder.build
|
162
|
+
@size = @layout.size
|
163
|
+
_class_meths_from_dsl_metadata( builder.metadata )
|
164
|
+
return @layout
|
165
|
+
end
|
166
|
+
|
167
|
+
def _class_meths_from_dsl_metadata(meta)
|
168
|
+
(@dsl_metadata = meta).each do |spec|
|
169
|
+
name = spec[:name]
|
170
|
+
type = spec[:type]
|
171
|
+
define_method(:"#{name}") do
|
172
|
+
self[name]
|
173
|
+
end unless instance_methods.include?(:"#{name}")
|
174
|
+
define_method(:"#{name}=") do |val|
|
175
|
+
self[name]=val
|
176
|
+
end unless instance_methods.include?(:"#{name}=")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def self.included(base)
|
182
|
+
base.extend(ClassMethods)
|
183
|
+
end
|
184
|
+
end # class StructHelper
|
185
|
+
|
186
|
+
# This is a wrapper around the FFI::StructLayoutBuilder. Its goal is to
|
187
|
+
# provides a more declarative syntax for defining structures and include
|
188
|
+
# the ability to attach arbitrary dsl_metadata information to structure
|
189
|
+
# fields during definition.
|
190
|
+
#
|
191
|
+
# The "DSL" (and that's really very in-quotes) supplies 3 ways to
|
192
|
+
# define a field (for now):
|
193
|
+
#
|
194
|
+
# field()
|
195
|
+
# array()
|
196
|
+
# struct()
|
197
|
+
#
|
198
|
+
# See the individual method descriptions for more info.
|
199
|
+
#
|
200
|
+
class DSL_StructLayoutBuilder
|
201
|
+
attr_reader :builder, :metadata
|
202
|
+
|
203
|
+
# Initializes the builder with a reference to the structure using it
|
204
|
+
# Instead of duplicating Struct features, we just call back to them.
|
205
|
+
def initialize(pbind)
|
206
|
+
@pbind = pbind
|
207
|
+
@builder = ::FFI::StructLayoutBuilder.new
|
208
|
+
@metadata = []
|
209
|
+
super()
|
210
|
+
end
|
211
|
+
|
212
|
+
# calls StructLayoutBuider.build() on the bulder and returns its
|
213
|
+
# result.
|
214
|
+
def build
|
215
|
+
@builder.build
|
216
|
+
end
|
217
|
+
|
218
|
+
# Calls StructLayoutBuilder.add_struct() on the builder and stores
|
219
|
+
# a metadata hash entry (the opts hash with name and type overridden)
|
220
|
+
#
|
221
|
+
# struct field_name, RubyClass, { ... metadata ... }
|
222
|
+
#
|
223
|
+
# :offset is a special key in metadata, specifies the offset of the field.
|
224
|
+
def struct(name, klass, o={})
|
225
|
+
unless klass.kind_of?(Class) and klass < ::FFI::Struct
|
226
|
+
raise(::ArgumentError, "klass must be a struct")
|
227
|
+
end
|
228
|
+
|
229
|
+
opts = o.merge(:name => name, :type => klass)
|
230
|
+
offset = opts[:offset]
|
231
|
+
ret=@builder.add_struct(name, klass, offset)
|
232
|
+
@metadata << opts
|
233
|
+
return ret
|
234
|
+
end
|
235
|
+
|
236
|
+
# Calls StructLayoutBuider.add_array() on the builder and stores
|
237
|
+
# a metadata hash entry (the opts hash with name and type overridden)
|
238
|
+
#
|
239
|
+
# Syntax:
|
240
|
+
#
|
241
|
+
# array field_name, [ctype, N], { ... metadata ... }
|
242
|
+
#
|
243
|
+
# :offset is a special key in metadata, specifies the offset of the field.
|
244
|
+
def array(name, type, o={})
|
245
|
+
unless type.kind_of?(::Array)
|
246
|
+
raise(::ArgumentError, "type must be an array")
|
247
|
+
end
|
248
|
+
|
249
|
+
opts = o.merge(:name => name, :type => type)
|
250
|
+
offset = opts[:offset]
|
251
|
+
mod = enclosing_module
|
252
|
+
ret=
|
253
|
+
if @builder.respond_to?(:add_array)
|
254
|
+
@builder.add_array(name, find_type(type[0], mod), type[1], offset)
|
255
|
+
else
|
256
|
+
@builder.add_field(name, type, offset)
|
257
|
+
end
|
258
|
+
|
259
|
+
@metadata << opts
|
260
|
+
return ret
|
261
|
+
end
|
262
|
+
|
263
|
+
# Calls StructLayoutBuider.add_field() on the builder and stores
|
264
|
+
# a metadata hash entry (the opts hash with name and type overridden)
|
265
|
+
#
|
266
|
+
# Syntax:
|
267
|
+
#
|
268
|
+
# field field_name, ctype, { ... metadata ... }
|
269
|
+
#
|
270
|
+
# :offset is a special key in metadata, specifies the offset of the field.
|
271
|
+
def field(name, type, o={})
|
272
|
+
opts = o.merge(:name => name, :type => type)
|
273
|
+
offset = opts[:offset]
|
274
|
+
mod = enclosing_module
|
275
|
+
ret= @builder.add_field(name, find_type(type, mod), offset)
|
276
|
+
@metadata << opts
|
277
|
+
return ret
|
278
|
+
end
|
279
|
+
|
280
|
+
def find_type(*args)
|
281
|
+
@pbind.find_type(*args)
|
282
|
+
end
|
283
|
+
|
284
|
+
def enclosing_module(*args)
|
285
|
+
@pbind.enclosing_module(*args)
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
289
|
+
|
290
|
+
# Used for creating various value <=> constant mapping namespace modules.
|
291
|
+
module ConstMap
|
292
|
+
|
293
|
+
def self.included(klass)
|
294
|
+
klass.extend(ConstMap)
|
295
|
+
end
|
296
|
+
|
297
|
+
# A flexible lookup. Takes 'arg' as a Symbol or String as a name to lookup
|
298
|
+
# a value, or an Integer to lookup a corresponding name.
|
299
|
+
def [](arg)
|
300
|
+
if arg.is_a? Integer
|
301
|
+
list.invert[arg]
|
302
|
+
elsif arg.is_a? String or arg.is_a? Symbol
|
303
|
+
list[arg.to_s.upcase]
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Generates a hash of all the constant names mapped to value. Usually,
|
308
|
+
# it's a good idea to override this like so in derived modules:
|
309
|
+
#
|
310
|
+
# def list; @@list = super() ; end
|
311
|
+
#
|
312
|
+
def list
|
313
|
+
constants.inject({}){|h,c| h.merge! c => const_get(c) }
|
314
|
+
end
|
315
|
+
|
316
|
+
private
|
317
|
+
# When called from a module definition or class method, this method
|
318
|
+
# imports all the constants from # namespace 'nspace' that start with
|
319
|
+
# into the local namespace as constants named with whatever follows the
|
320
|
+
# prefix. Only constant names that match [A-Z][A-Z0-9_]+ are imported,
|
321
|
+
# the rest are ignored.
|
322
|
+
#
|
323
|
+
# This method also yields the (short) constant name and value to a block
|
324
|
+
# if one is provided. The block works like [...].select {|c,v| ... } in
|
325
|
+
# that the value is not mapped if the block returns nil or false.
|
326
|
+
def slurp_constants(nspace, prefix)
|
327
|
+
nspace.constants.grep(/^(#{prefix}([A-Z][A-Z0-9_]+))$/) do
|
328
|
+
c = $2
|
329
|
+
v = nspace.const_get($1)
|
330
|
+
next if block_given? and not yield(c,v)
|
331
|
+
const_set c, v
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
# Behaves just like ConstFlags, except that the [nnn] returns a list
|
337
|
+
# of names for the flags set on nnn. Name string lookups work same way as
|
338
|
+
# ConstFlags.
|
339
|
+
module ConstFlagsMap
|
340
|
+
include ConstMap
|
341
|
+
|
342
|
+
def self.included(klass)
|
343
|
+
klass.extend(ConstFlagsMap)
|
344
|
+
end
|
345
|
+
|
346
|
+
# A flexible lookup. Takes 'arg' as a Symbol or String as a name to lookup
|
347
|
+
# a bit-flag value, or an Integer to lookup a corresponding names for the
|
348
|
+
# flags present in it.
|
349
|
+
def [](arg)
|
350
|
+
if arg.is_a? Integer
|
351
|
+
ret = []
|
352
|
+
if arg == 0
|
353
|
+
n = list.invert[0]
|
354
|
+
ret << n if n
|
355
|
+
else
|
356
|
+
list.invert.sort.each {|v,n| ret << n if v !=0 and (v & arg) == v }
|
357
|
+
end
|
358
|
+
return ret
|
359
|
+
elsif arg.is_a? String or arg.is_a? Symbol
|
360
|
+
list[arg.to_s.upcase]
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
|
data/lib/ffi_dry.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ffi/dry'
|
data/samples/afmap.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'ffi/dry'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
module AddressFamily
|
5
|
+
include FFI::DRY::ConstMap
|
6
|
+
slurp_constants ::Socket, "AF_"
|
7
|
+
def list ; @@list ||= super() ; end
|
8
|
+
end
|
9
|
+
|
10
|
+
# AddressFamily now has all the constants it found for Socket::AF_*
|
11
|
+
#
|
12
|
+
# i.e. AddressFamily::INET
|
13
|
+
# AddressFamily::LINK
|
14
|
+
# AddressFamily::INET6
|
15
|
+
# etc...
|
16
|
+
#
|
17
|
+
# We can do quick lookups
|
18
|
+
# AddressFamily[2] # => "INET"
|
19
|
+
# AddressFamily["INET"] # => 2
|
20
|
+
#
|
21
|
+
# We can get a hash of all key-value pairs with .list
|
22
|
+
# AddressFamily.list
|
23
|
+
# # => {"NATM"=>31, "DLI"=>13, "UNIX"=>1, "NETBIOS"=>33, ...}
|
24
|
+
#
|
25
|
+
# ... which can be inverted for a reverse mapping
|
26
|
+
# AddressFamily.list.invert
|
27
|
+
# # => {16=>"APPLETALK", 5=>"CHAOS", 27=>"NDRV", 0=>"UNSPEC", ...}
|
28
|
+
#
|
data/samples/basic.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# One major feature is a dsl"-like" syntax for declaring structure members
|
3
|
+
# in FFI::Struct or FFI::ManagedStruct definitions.
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'ffi'
|
7
|
+
require 'ffi/dry'
|
8
|
+
|
9
|
+
class SomeStruct < FFI::Struct
|
10
|
+
include FFI::DRY::StructHelper
|
11
|
+
|
12
|
+
# we get a new way of specifying layouts with a 'dsl'-like syntax
|
13
|
+
# The hash containing {:desc => ... } can contain arbitrary keys which
|
14
|
+
# can be used however we like. dsl_metadata will contain all these
|
15
|
+
# in the class and instance.
|
16
|
+
dsl_layout do
|
17
|
+
field :field1, :uint16, :desc => 'this is field 1'
|
18
|
+
field :field2, :uint16, :desc => 'this is field 2'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
ss0=SomeStruct.new
|
23
|
+
|
24
|
+
# With the declaration above, we specified :desc hash value, which was stored
|
25
|
+
# in metadata along with the field name and type.
|
26
|
+
|
27
|
+
pp ss0.dsl_metadata
|
28
|
+
[{:type=>:uint16, :name=>:field1, :desc=>"this is field 1"},
|
29
|
+
{:type=>:uint16, :name=>:field2, :desc=>"this is field 2"}]
|
30
|
+
# => nil
|
31
|
+
|
32
|
+
|
33
|
+
# We get some free additional ways of instantiating and declaring values during
|
34
|
+
# initialization. (The FFI standard ways still work too)
|
35
|
+
|
36
|
+
raw_data = "\x00\x00\xff\xff"
|
37
|
+
|
38
|
+
ss1=SomeStruct.new :raw => raw_data
|
39
|
+
ss2=SomeStruct.new :raw => raw_data, :field1 => 1, :field2 => 2
|
40
|
+
ss3=SomeStruct.new {|x| x.field1=1 }
|
41
|
+
ss4=SomeStruct.new(:raw => raw_data) {|x| x.field1=1 }
|
42
|
+
|
43
|
+
[ ss0,
|
44
|
+
ss1,
|
45
|
+
ss2,
|
46
|
+
ss3,
|
47
|
+
ss4 ].each_with_index {|x,i| p ["ss#{i}",[x.field1, x.field2]]}
|
48
|
+
|
49
|
+
# which should produce...
|
50
|
+
# ["ss0", [0, 0]]
|
51
|
+
# ["ss1", [0, 65535]]
|
52
|
+
# ["ss2", [1, 2]]
|
53
|
+
# ["ss3", [1, 0]]
|
54
|
+
# ["ss4", [1, 65535]]
|
55
|
+
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# Here's a broader example which utilizes that arbitrary ':desc' parameter in a
|
2
|
+
# "neighborly" way. This also demonstrates superclasses to add common struct
|
3
|
+
# features, declaring array fields, as well as nesting other structs.
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'ffi'
|
7
|
+
require 'ffi/dry'
|
8
|
+
|
9
|
+
class NeighborlyStruct < ::FFI::Struct
|
10
|
+
include ::FFI::DRY::StructHelper
|
11
|
+
|
12
|
+
def self.describe
|
13
|
+
print "Struct: #{self.name}"
|
14
|
+
dsl_metadata().each_with_index do |spec, i|
|
15
|
+
print " Field #{i}\n"
|
16
|
+
print " name: #{spec[:name].inspect}\n"
|
17
|
+
print " type: #{spec[:type].inspect}\n"
|
18
|
+
print " desc: #{spec[:desc]}\n\n"
|
19
|
+
end
|
20
|
+
print "\n"
|
21
|
+
end
|
22
|
+
def describe; self.class.describe; end
|
23
|
+
end
|
24
|
+
|
25
|
+
class TestStruct < NeighborlyStruct
|
26
|
+
dsl_layout do
|
27
|
+
field :field1, :uint8, :desc => "test field 1"
|
28
|
+
field :field2, :uint8, :desc => "test field 2"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class SomeStruct < NeighborlyStruct
|
33
|
+
dsl_layout do
|
34
|
+
field :kind, :uint8, :desc => "a type identifier"
|
35
|
+
struct :tst, TestStruct, :desc => "a nested TestStruct"
|
36
|
+
field :len, :uint8, :desc => "8-bit size value (>= self.size+2)"
|
37
|
+
array :str, [:char,255],
|
38
|
+
:desc => "a string up to 255 bytes bound by :len"
|
39
|
+
end
|
40
|
+
|
41
|
+
# override kind getter method with our own
|
42
|
+
# resolves kind to some kind of type array for example...
|
43
|
+
def kind
|
44
|
+
[:default, :bar, :baz][ self[:kind] ]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
s1=TestStruct.new
|
49
|
+
s2=SomeStruct.new
|
50
|
+
|
51
|
+
# check out that 'kind' override:
|
52
|
+
s2.kind
|
53
|
+
# => :default
|
54
|
+
|
55
|
+
# oh and the regular FFI way is always intact
|
56
|
+
s2[:kind]
|
57
|
+
# => 0
|
58
|
+
|
59
|
+
s2[:kind]=1
|
60
|
+
s2.kind
|
61
|
+
# => :bar
|
62
|
+
|
63
|
+
s2.kind=3
|
64
|
+
s2.kind
|
65
|
+
# => :baz
|
66
|
+
|
67
|
+
puts "*"*70
|
68
|
+
s1.describe
|
69
|
+
## we get a dump of metadata
|
70
|
+
# **********************************************************************
|
71
|
+
# Struct: TestStruct
|
72
|
+
# Field 0
|
73
|
+
# name: :field1
|
74
|
+
# type: :uint8
|
75
|
+
# desc: test field 1
|
76
|
+
#
|
77
|
+
# Field 1
|
78
|
+
# name: :field2
|
79
|
+
# type: :uint8
|
80
|
+
# desc: test field 2
|
81
|
+
|
82
|
+
puts "*"*70
|
83
|
+
s2.describe
|
84
|
+
## we get a dump of metadata
|
85
|
+
# Struct: SomeStruct Field 0
|
86
|
+
# name: :kind
|
87
|
+
# type: :uint8
|
88
|
+
# desc: a type identifier
|
89
|
+
#
|
90
|
+
# Field 1
|
91
|
+
# name: :tst
|
92
|
+
# type: TestStruct
|
93
|
+
# desc: a nested TestStruct
|
94
|
+
#
|
95
|
+
# Field 2
|
96
|
+
# name: :len
|
97
|
+
# type: :uint8
|
98
|
+
# desc: 8-bit size value (>= self.size+2)
|
99
|
+
#
|
100
|
+
# Field 3
|
101
|
+
# name: :str
|
102
|
+
# type: [:char, 255]
|
103
|
+
# desc: a string up to 255 bytes bound by :len
|
104
|
+
|
105
|
+
puts "*"*70
|
106
|
+
s2.tst.describe
|
107
|
+
## same as s1.describe
|
108
|
+
# **********************************************************************
|
109
|
+
# Struct: TestStruct
|
110
|
+
# Field 0
|
111
|
+
# name: :field1
|
112
|
+
# type: :uint8
|
113
|
+
# desc: test field 1
|
114
|
+
#
|
115
|
+
# Field 1
|
116
|
+
# name: :field2
|
117
|
+
# type: :uint8
|
118
|
+
# desc: test field 2
|
119
|
+
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ffi_dry
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eric Monti
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-16 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: yard
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: Provides a some useful modules, classes, and methods as well as a DSL-like syntax for FFI::Struct layouts
|
36
|
+
email: emonti@matasano.com
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files:
|
42
|
+
- LICENSE
|
43
|
+
- README.rdoc
|
44
|
+
files:
|
45
|
+
- .document
|
46
|
+
- .gitignore
|
47
|
+
- LICENSE
|
48
|
+
- README.rdoc
|
49
|
+
- Rakefile
|
50
|
+
- VERSION
|
51
|
+
- ffi_dry.gemspec
|
52
|
+
- lib/ffi/dry.rb
|
53
|
+
- lib/ffi_dry.rb
|
54
|
+
- samples/afmap.rb
|
55
|
+
- samples/basic.rb
|
56
|
+
- samples/describer.rb
|
57
|
+
- spec/ffi_dry_spec.rb
|
58
|
+
- spec/spec_helper.rb
|
59
|
+
has_rdoc: true
|
60
|
+
homepage: http://github.com/emonti/ffi_dry
|
61
|
+
licenses: []
|
62
|
+
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options:
|
65
|
+
- --charset=UTF-8
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: "0"
|
73
|
+
version:
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: "0"
|
79
|
+
version:
|
80
|
+
requirements: []
|
81
|
+
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 1.3.4
|
84
|
+
signing_key:
|
85
|
+
specification_version: 3
|
86
|
+
summary: Syntactic sugar and helper utilities for FFI
|
87
|
+
test_files:
|
88
|
+
- spec/ffi_dry_spec.rb
|
89
|
+
- spec/spec_helper.rb
|