nice-ffi 0.1 → 0.2

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/README.rdoc CHANGED
@@ -1,8 +1,8 @@
1
1
 
2
2
  = Nice-FFI
3
3
 
4
- Version:: 0.1
5
- Date:: 2009-07-05
4
+ Version:: 0.2
5
+ Date:: 2009-10-24
6
6
 
7
7
  Homepage:: http://github.com/jacius/nice-ffi/
8
8
  Author:: John Croisant <jacius@gmail.com>
@@ -12,22 +12,30 @@ Copyright:: 2009 John Croisant
12
12
  == Description
13
13
 
14
14
  Nice-FFI is a layer on top of Ruby-FFI [1] (and compatible FFI
15
- systems) to augment it with features to aid development of
16
- FFI-based libraries.
15
+ systems) with features to ease development of FFI-based libraries.
17
16
 
18
17
  Nice-FFI currently features:
19
18
 
20
- * NiceFFI::Struct: a stand-in for FFI::Struct that provides automatic
21
- accessors for struct members, more instance initialization options,
22
- pretty to_s and inspect methods, and other niceties.
23
-
24
19
  * NiceFFI::Library: a stand-in for FFI::Library that provides methods
25
20
  for easily finding and loading libraries on any platform, plus
26
21
  automatic wrapping of functions that return struct pointers.
27
22
 
23
+ * NiceFFI::PathSet: a class with customizable rules for finding
24
+ library files on multiple operating system. PathSet is used by
25
+ NiceFFI::Library.load_library.
26
+
27
+ * NiceFFI::Struct: a stand-in for FFI::Struct that provides automatic
28
+ accessors for struct members, optional automatic memory management,
29
+ more instance initialization options, pretty to_s and inspect
30
+ methods, and other niceties.
31
+
32
+ * NiceFFI::OpaqueStruct: a base class for structs with no user-exposed
33
+ members. Useful when the struct definition is hidden by the
34
+ underlying C library.
35
+
28
36
  Nice-FFI was originally developed as part of Ruby-SDL-FFI [2].
29
37
 
30
- 1. Ruby-FFI: http://kenai.com/projects/ruby-ffi/
38
+ 1. Ruby-FFI: http://github.com/ffi/ffi
31
39
  2. Ruby-SDL-FFI: http://github.com/jacius/ruby-sdl-ffi/
32
40
 
33
41
 
@@ -46,8 +54,12 @@ match the new API, then you should wait until version 1.0.
46
54
 
47
55
  == Requirements
48
56
 
49
- * Ruby-FFI >= 0.3.0 (or compatible)
50
- * need >= 1.1.0
57
+ * Ruby-FFI >= 0.4.0 (or compatible FFI implementation)
58
+
59
+
60
+ == Usage
61
+
62
+ See docs/usage.rdoc for usage information.
51
63
 
52
64
 
53
65
  == License
data/docs/usage.rdoc CHANGED
@@ -30,14 +30,14 @@ of "extend FFI::Library", and use "load_library" instead of "ffi_lib":
30
30
 
31
31
 
32
32
  require 'nice-ffi'
33
-
33
+
34
34
  module MyLibraryModule
35
35
  extend NiceFFI::Library
36
-
36
+
37
37
  load_library("SDL") # look for libSDL.so, SDL.dll, etc.
38
-
38
+
39
39
  # structs, functions, etc. as usual.
40
-
40
+
41
41
  end
42
42
 
43
43
 
@@ -47,53 +47,72 @@ As mentioned, load_library uses NiceFFI::PathSet to search for the
47
47
  library in likely directories. Specifically, it looks for:
48
48
 
49
49
 
50
- NiceFFI::Library::DEFAULT_PATHS = NiceFFI::PathSet.new(
51
-
52
- /linux|bsd/ => [ "/usr/local/lib/lib[NAME].so",
53
- "/usr/lib/lib[NAME].so",
54
- "[NAME]" ],
55
-
56
- /darwin/ => [ "/usr/local/lib/lib[NAME].dylib",
57
- "/sw/lib/lib[NAME].dylib",
58
- "/opt/local/lib/lib[NAME].dylib",
59
- "~/Library/Frameworks/[NAME].framework/[NAME]",
60
- "/Library/Frameworks/[NAME].framework/[NAME]",
61
- "[NAME]" ],
62
-
63
- /win32/ => [ "C:\\windows\\system32\\[NAME].dll",
64
- "C:\\windows\\system\\[NAME].dll",
65
- "[NAME]" ]
66
- )
50
+ paths = {
51
+ /linux|bsd/ => [ "/usr/local/lib/",
52
+ "/usr/lib/" ],
53
+
54
+ /darwin/ => [ "/usr/local/lib/",
55
+ "/sw/lib/",
56
+ "/opt/local/lib/",
57
+ "~/Library/Frameworks/",
58
+ "/Library/Frameworks/" ],
59
+
60
+ /windows/ => [ "C:\\windows\\system32\\",
61
+ "C:\\windows\\system\\" ]
62
+ }
63
+
64
+ files = {
65
+ /linux|bsd/ => [ "lib[NAME].so" ],
66
+
67
+ /darwin/ => [ "lib[NAME].dylib",
68
+ "[NAME].framework/[NAME]" ],
69
+
70
+ /windows/ => [ "[NAME].dll" ]
71
+ }
72
+
73
+ NiceFFI::PathSet::DEFAULT = NiceFFI::PathSet.new( paths, files )
67
74
 
68
75
 
69
- The string "[NAME]" is replaced with whatever string you pass to
76
+ The paths hash tells PathSet where to look for libraries, and the
77
+ files hash tells it the format of the library filename itself. The
78
+ string "[NAME]" is replaced with whatever string you pass to
70
79
  load_library.
71
80
 
81
+ Each key in the hash should be a Regexp that matches an OS name from
82
+ FFI::Platform::OS. As of this writing (October 2009), the list of
83
+ recognized OS names is:
84
+
85
+ * "darwin" (MacOS X)
86
+ * "freebsd"
87
+ * "linux"
88
+ * "openbsd"
89
+ * "solaris"
90
+ * "windows"
91
+
72
92
  So, if the user is running Linux and you try to load "SDL", it will
73
- first look for "/usr/local/lib/libSDL.so". If it couldn't find the
74
- first one, it will then look for "/usr/lib/libSDL.so", and finally it
75
- will just try loading "SDL" using ffi_lib (which does some
76
- platform-appropriate guesses too). It would also use those same
77
- paths for FreeBSD, because that OS matches /linux|bsd/, too.
78
- /darwin/ matches on MacOS X, and /win32/ matches on Windows.
93
+ first look for "/usr/local/lib/libSDL.so". If it can't find that, it
94
+ will then look for "/usr/lib/libSDL.so". It would also use those same
95
+ paths for FreeBSD or OpenBSD, because those OS names also match the
96
+ regexp /linux|bsd/.
97
+
98
+ If the library could not be found in any of the given directories with
99
+ the given file name formats, load_library will just try loading "SDL"
100
+ using ffi_lib (which does some platform-appropriate guesses too). If
101
+ that fails too, LoadError is raised.
79
102
 
80
103
  If you want to load from a different path, you can make a custom
81
104
  PathSet and pass it to load_library:
82
105
 
83
106
 
84
- this_dir = File.dirname(__FILE__)
107
+ libs_dir = File.dirname(__FILE__) + "/libs/"
85
108
 
86
- my_pathset = NiceFFI::Library::DEFAULT_PATHS.prepend(
87
- /linux|bsd/ => [ "#{this_dir}/libs/lib[NAME].so" ],
88
- /darwin/ => [ "#{this_dir}/libs/lib[NAME].dylib" ],
89
- /win32/ => [ "#{this_dir}/libs/[NAME].dll" ]
90
- )
109
+ pathset = NiceFFI::PathSet::DEFAULT.prepend( libs_dir )
91
110
 
92
111
  load_library( "SDL", my_pathset )
93
112
 
94
113
 
95
114
  The above example prepends (adds in front) the new paths so
96
- that load_library will look for the library in in "./libs/" first.
115
+ that load_library will look for the library in "./libs/" first.
97
116
  See PathSet for other useful methods for modifying PathSets.
98
117
 
99
118
 
@@ -135,8 +154,12 @@ With TypedPointer, the wrapping happens automatically. Just attach
135
154
  the function with a TypedPointer instead of :pointer:
136
155
 
137
156
 
138
- attach_function :get_my_struct, [:int], NiceFFI::TypedPointer( MyStruct )
157
+ attach_function :make_my_struct, [:int, :int], NiceFFI::TypedPointer( MyStruct )
139
158
 
159
+ # If MyStruct is based on NiceFFI::Struct, you can do this instead:
160
+
161
+ attach_function :make_my_struct, [:int, :int], MyStruct.typed_pointer
162
+
140
163
 
141
164
  Then you automatically get a MyStruct instance when you call the function:
142
165
 
@@ -148,7 +171,6 @@ Then you automatically get a MyStruct instance when you call the function:
148
171
  Voila!
149
172
 
150
173
 
151
-
152
174
  == NiceFFI::Struct
153
175
 
154
176
  NiceFFI::Struct is a replacement for FFI::Struct. It provides several
@@ -252,3 +274,88 @@ struct pointer, but if you specify a TypedPointer instead of
252
274
  # Seamlessly unwraps the struct and stores the pointer
253
275
  struct.my = MyStruct.new([-4,-3])
254
276
 
277
+
278
+ === Automatic Memory Managment
279
+
280
+ Ruby-FFI already provides automatic memory management when you create
281
+ a FFI::MemoryPointer or FFI::Buffer instance. When those instances are
282
+ garbage collected, their memory is automatically released so it can be
283
+ used elsewhere.
284
+
285
+ That feature is used by NiceFFI::Struct when you create a new instance
286
+ by passing a Hash, Array, String, or another instance. In those cases,
287
+ new memory is allocated for the struct instance, and automatically
288
+ released when the struct instance is galbage collected.
289
+
290
+ NiceFFI::Struct also provides an optional automatic memory management
291
+ system for normal pointers. To use this system, define a "release"
292
+ class method in your class. Then if you create a new struct instance
293
+ with an FFI::Pointer, the release class method will automatically be
294
+ called when the memory for a struct instance needs to be freed.
295
+
296
+ (This also applies to attached functions with TypedPointer return
297
+ values. The pointers returned from those functions are wrapped in the
298
+ struct class, so if you have defined the release class method, they
299
+ will be automatically memory managed.)
300
+
301
+ The release class method must accept an FFI::Pointer and call an
302
+ appropriate function to free the struct's memory. Here's an example
303
+ from Ruby-SDL-FFI:
304
+
305
+
306
+ class Surface < NiceFFI::Struct
307
+
308
+ def self.release( pointer )
309
+ SDL.FreeSurface( pointer )
310
+ end
311
+
312
+ # ...
313
+
314
+ end
315
+
316
+
317
+ Note: the release class method should not have any side effects
318
+ besides freeing the struct's memory. Don't be clever!
319
+
320
+ The memory management system keeps a reference count for each pointer
321
+ address, so the release class method will only be called when all
322
+ struct instances that are using that memory have been garbage
323
+ collected. That means it's safe to have many instances sharing the
324
+ same memory.
325
+
326
+ If you want to create an instance that doesn't use the memory
327
+ management system, you can disable the :autorelease option when
328
+ creating the instance, like so:
329
+
330
+
331
+ struct = MyStructClass.new( a_pointer, :autorelease => false )
332
+
333
+
334
+ == NiceFFI::OpaqueStruct
335
+
336
+ Some C libraries have structs with no publicly-visible layout.
337
+ Instead, the internal details are hidden, and only modified by calling
338
+ functions in the library.
339
+
340
+ For example, the SDL_mixer library has this definition in its header
341
+ file:
342
+
343
+
344
+ typedef struct _Mix_Music Mix_Music;
345
+
346
+
347
+ "_Mix_Music" is a struct that is defined within SDL_mixer, but its
348
+ internals are different depending on what features SDL_mixer was
349
+ compiled with. The struct members are not revealed in the header file,
350
+ so they can't be accessed like a normal struct.
351
+
352
+ NiceFFI provides a class for handling special cases like this,
353
+ NiceFFI::OpaqueStruct. OpaqueStruct has no layout and no members, and
354
+ cannot be created by passing in Hashes, Arrays, etc. It simply holds a
355
+ pointer to the struct memory. As with NiceStruct (and FFI::Struct),
356
+ instances of OpaqueStruct-based classes can be passed directly to
357
+ functions expecting a pointer of the appropriate struct type.
358
+
359
+ OpaqueStruct features the same optional memory management system as
360
+ NiceStruct. Read the "Automatic Memory Management" section above for
361
+ information about how to use this feature.
data/lib/nice-ffi.rb CHANGED
@@ -28,20 +28,26 @@
28
28
  #++
29
29
 
30
30
 
31
- require 'need'
31
+ require 'ffi'
32
32
 
33
33
 
34
34
  module NiceFFI
35
35
  end
36
36
 
37
37
 
38
+ this_dir = File.expand_path( File.dirname(__FILE__) )
39
+
38
40
  %w{
39
41
 
40
- nicelibrary
41
- nicestruct
42
+ typedpointer
43
+ pathset
44
+ library
45
+ autorelease
46
+ struct
47
+ opaquestruct
42
48
 
43
49
  }.each do |f|
44
50
 
45
- need { File.join( 'nice-ffi', f ) }
51
+ require File.join( this_dir, 'nice-ffi', f )
46
52
 
47
53
  end
@@ -0,0 +1,112 @@
1
+ #--
2
+ #
3
+ # This file is one part of:
4
+ #
5
+ # Nice-FFI - Convenience layer atop Ruby-FFI
6
+ #
7
+ # Copyright (c) 2009 John Croisant
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining
10
+ # a copy of this software and associated documentation files (the
11
+ # "Software"), to deal in the Software without restriction, including
12
+ # without limitation the rights to use, copy, modify, merge, publish,
13
+ # distribute, sublicense, and/or sell copies of the Software, and to
14
+ # permit persons to whom the Software is furnished to do so, subject to
15
+ # the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be
18
+ # included in all copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
25
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
+ #
28
+ #++
29
+
30
+
31
+
32
+ #--
33
+ # Don't scan this module for RDoc/RI.
34
+
35
+
36
+ require 'thread' # for Mutex. JRuby doesn't load this by default.
37
+
38
+ # A mixin module to provide automatic memory management for C structs.
39
+ #
40
+ module NiceFFI::AutoRelease
41
+
42
+ # Sets up the class when this module is included. Adds the class
43
+ # methods and defines class instance variables.
44
+ def self.included( klass )
45
+ class << klass
46
+
47
+ # Increment the reference count for this address.
48
+ def _incr_refcount( address )
49
+ @ptr_mutex ||= Mutex.new
50
+ @ptr_mutex.synchronize {
51
+ @ptr_refcounts ||= Hash.new(0)
52
+ @ptr_refcounts[address] += 1
53
+ }
54
+ return nil
55
+ end
56
+
57
+ # Decrement the counter for this pointer's address, and free
58
+ # the memory if the reference count falls below 1.
59
+ #
60
+ def _release( pointer )
61
+ @ptr_mutex ||= Mutex.new
62
+ @ptr_mutex.synchronize {
63
+ _decr_refcount(pointer.address)
64
+ if( @ptr_refcounts[pointer.address] < 1 )
65
+ release( pointer )
66
+ end
67
+ }
68
+ end
69
+
70
+ private
71
+
72
+ # Decrement the reference count for this address. If the count falls
73
+ # below 1, the address is removed from Hash altogether.
74
+ #
75
+ # Note: this method does not have a Mutex lock by itself, but you
76
+ # should use a lock in any methods that call it.
77
+ #
78
+ def _decr_refcount( address )
79
+ @ptr_refcounts ||= Hash.new(0)
80
+ @ptr_refcounts[address] -= 1
81
+ if( @ptr_refcounts[address] < 1 )
82
+ @ptr_refcounts.delete(address)
83
+ end
84
+ end
85
+
86
+ end
87
+ end
88
+
89
+
90
+ private
91
+
92
+
93
+ def _make_autopointer( ptr, autorelease=true )
94
+ if( autorelease and
95
+ self.class.respond_to?(:release) and
96
+ ptr.is_a?(FFI::Pointer) and
97
+ not ptr.is_a?(FFI::MemoryPointer) and
98
+ not ptr.is_a?(FFI::Buffer) )
99
+
100
+ # Increment the reference count for this pointer address
101
+ self.class._incr_refcount( ptr.address )
102
+
103
+ # Wrap in an AutoPointer to call self.class._release when it's GC'd.
104
+ return FFI::AutoPointer.new( ptr, self.class.method(:_release) )
105
+
106
+ else
107
+ return ptr
108
+ end
109
+ end
110
+
111
+
112
+ end
@@ -28,49 +28,33 @@
28
28
  #++
29
29
 
30
30
 
31
- require 'ffi'
32
-
33
- need{ 'typedpointer' }
34
- need{ 'pathset' }
35
-
36
-
37
31
  # A module to be used in place of FFI::Library. It acts mostly
38
32
  # like FFI::Library, but with some nice extra features and
39
33
  # conveniences to make life easier:
40
34
  #
35
+ # * load_library method to help find and load libraries from common
36
+ # (or custom) places for the current OS. Use it instead of ffi_lib.
37
+ #
41
38
  # * attach_function accepts TypedPointers as return type,
42
39
  # in which case it wraps the return value of the bound function
43
40
  # in the TypedPointer's type.
44
41
  #
42
+ # * Shorthand aliases to improve code readability:
43
+ # * func = attach_function
44
+ # * var = attach_variable
45
+ #
45
46
  module NiceFFI::Library
46
47
 
47
48
  def self.extend_object( klass )
48
49
  klass.extend FFI::Library
49
- super
50
- end
51
-
52
-
53
- # The default paths to look for libraries. See PathSet
54
- # and #load_library.
55
- #
56
- DEFAULT_PATHS = NiceFFI::PathSet.new(
57
-
58
- /linux|bsd/ => [ "/usr/local/lib/lib[NAME].so",
59
- "/usr/lib/lib[NAME].so",
60
- "[NAME]" ],
61
-
62
- /darwin/ => [ "/usr/local/lib/lib[NAME].dylib",
63
- "/sw/lib/lib[NAME].dylib",
64
- "/opt/local/lib/lib[NAME].dylib",
65
- "~/Library/Frameworks/[NAME].framework/[NAME]",
66
- "/Library/Frameworks/[NAME].framework/[NAME]",
67
- "[NAME]" ],
68
50
 
69
- /win32/ => [ "C:\\windows\\system32\\[NAME].dll",
70
- "C:\\windows\\system\\[NAME].dll",
71
- "[NAME]" ]
51
+ super
72
52
 
73
- )
53
+ class << klass
54
+ alias :func :attach_function
55
+ alias :var :attach_variable
56
+ end
57
+ end
74
58
 
75
59
 
76
60
  # Try to find and load a library (e.g. "SDL_ttf") into an FFI
@@ -81,22 +65,14 @@ module NiceFFI::Library
81
65
  #
82
66
  # Raises LoadError if it could not find or load the library.
83
67
  #
84
- def load_library( names, search_paths=NiceFFI::Library::DEFAULT_PATHS )
68
+ def load_library( names, search_paths=NiceFFI::PathSet::DEFAULT_PATHS )
85
69
 
86
70
  names = [names] unless names.kind_of? Array
87
71
 
88
72
  paths = search_paths.find( *names )
89
73
 
90
- pretty_names = if names.size == 1
91
- names[0]
92
- else
93
- names[0..-2].join(", ") + ", or " + names[-1]
94
- end
95
-
96
- # Oops, couldn't find it anywhere.
97
- if paths.empty?
98
- raise LoadError, "Could not find #{pretty_names}. Is it installed?"
99
- end
74
+ # Try just the plain library name(s), as last resort.
75
+ paths += names
100
76
 
101
77
  # Try loading each path until one works.
102
78
  loaded = paths.find { |path|
@@ -111,8 +87,14 @@ module NiceFFI::Library
111
87
  end
112
88
  }
113
89
 
114
- # Oops, none of them worked.
115
90
  if loaded.nil?
91
+ # Oops, none of them worked.
92
+ pretty_names = if names.size == 1
93
+ names[0]
94
+ else
95
+ names[0..-2].join(", ") + ", or " + names[-1]
96
+ end
97
+
116
98
  raise( LoadError, "Could not load #{pretty_names}." )
117
99
  else
118
100
  # Return the one that did work
@@ -130,10 +112,10 @@ module NiceFFI::Library
130
112
  # 2. methname, funcname, args, retrn_type
131
113
  #
132
114
  funcname, args, retrn_type = if arg1.kind_of?(Array)
133
- [methname, arg1, arg2]
134
- else
135
- [arg1, arg2, arg3]
136
- end
115
+ [methname, arg1, arg2]
116
+ else
117
+ [arg1, arg2, arg3]
118
+ end
137
119
 
138
120
  unless retrn_type.kind_of? NiceFFI::TypedPointer
139
121
  # Normal FFI::Library.attach_function behavior.
@@ -169,4 +151,50 @@ module NiceFFI::Library
169
151
  end
170
152
 
171
153
  end
154
+
155
+
156
+ # Calls the given block, but rescues and prints a warning if
157
+ # FFI::NotFoundError is raised. If warn_message is nil, the
158
+ # error message is printed instead.
159
+ #
160
+ # This is intended to be used around #attach_function for cases
161
+ # where the function may not exist (e.g. because the user has an
162
+ # old version of the library) and the library should continue loading
163
+ # anyway (with the function missing).
164
+ #
165
+ #
166
+ # Example:
167
+ #
168
+ # module Foo
169
+ # extend NiceFFI::Library
170
+ #
171
+ # load_library( "libfoo" )
172
+ #
173
+ # optional( "Warning: Your libfoo doesn't have opt_func()" ) do
174
+ # attach_function :opt_func, [], :int
175
+ # end
176
+ # end
177
+ #
178
+ def optional( warn_message=nil, &block )
179
+ raise LocalJumpError, "no block given" unless block_given?
180
+ begin
181
+ block.call()
182
+ rescue FFI::NotFoundError => e
183
+ if warn_message
184
+ puts warn_message
185
+ else
186
+ puts "Warning: #{e.message}"
187
+ end
188
+ end
189
+ end
190
+
191
+
192
+ # Like #attach_function, but wrapped in #optional.
193
+ def optional_function( *args )
194
+ optional { attach_function( *args ) }
195
+ end
196
+
197
+ alias :optfunc :optional_function
198
+
199
+
172
200
  end