ffi_dry 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|