nice-ffi 0.1 → 0.2

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