Add Function Version Support #145

Closed
wants to merge 3 commits into from
View
30 lib/ffi/dynamic_library.rb
@@ -0,0 +1,30 @@
+# Copyright (C) 2008, 2009 Wayne Meissner
+# Copyright (c) 2007, 2008 Evan Phoenix
+# All rights reserved.
+#
+# This file is part of ruby-ffi.
+#
+# This code is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License version 3 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+# version 3 for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# version 3 along with this work. If not, see <http://www.gnu.org/licenses/>.
+#
+
+module FFI
+ class DynamicLibrary
+ def version
+ @version ||= Version.new(nil)
+ end
+
+ def version=(value)
+ @version = Version.new(value)
+ end
+ end
+end
View
3 lib/ffi/ffi.rb
@@ -31,3 +31,6 @@
require 'ffi/autopointer'
require 'ffi/variadic'
require 'ffi/enum'
+require 'ffi/version'
+require 'ffi/not_implemented'
+require 'ffi/dynamic_library'
View
50 lib/ffi/library.rb
@@ -222,44 +222,44 @@ def attach_function(name, func, args, returns = nil, options = nil)
@blocking = false
options.merge!(opts) if opts && opts.is_a?(Hash)
+ required_version = Version.new(options[:version])
# Try to locate the function in any of the libraries
- invokers = []
- ffi_libraries.each do |lib|
- if invokers.empty?
- begin
- function = nil
- function_names(cname, arg_types).find do |fname|
- function = lib.find_function(fname)
- end
- raise LoadError unless function
+ invoker = nil
+ ffi_libraries.find do |lib|
+ begin
+ invoker =
+ if lib.version < required_version
+ NotImplemented.new(cname, required_version, lib.version)
+ else
+ function = find_decorated_function(lib, cname, arg_types)
+ raise LoadError unless function
- invokers << if arg_types.length > 0 && arg_types[arg_types.length - 1] == FFI::NativeType::VARARGS
- VariadicInvoker.new(function, arg_types, find_type(ret_type), options)
+ if arg_types.length > 0 && arg_types[arg_types.length - 1] == FFI::NativeType::VARARGS
+ VariadicInvoker.new(function, arg_types, find_type(ret_type), options)
- else
- Function.new(find_type(ret_type), arg_types, function, options)
+ else
+ Function.new(find_type(ret_type), arg_types, function, options)
+ end
end
-
- rescue LoadError
- end
+ rescue LoadError => ex
end
end
- invoker = invokers.compact.shift
raise FFI::NotFoundError.new(cname.to_s, ffi_libraries.map { |lib| lib.name }) unless invoker
invoker.attach(self, mname.to_s)
invoker
end
- # @param [#to_s] name function name
- # @param [Array] arg_types function's argument types
- # @return [Array<String>]
- # This function returns a list of possible names to lookup.
- # @note Function names on windows may be decorated if they are using stdcall. See
- # * http://en.wikipedia.org/wiki/Name_mangling#C_name_decoration_in_Microsoft_Windows
- # * http://msdn.microsoft.com/en-us/library/zxk0tw93%28v=VS.100%29.aspx
- # * http://en.wikibooks.org/wiki/X86_Disassembly/Calling_Conventions#STDCALL
+ def find_decorated_function(lib, name, arg_types)
+ result = nil
+ map_function_name(name, arg_types).find do |decorated_name|
+ result = lib.find_function(decorated_name)
+ end
+ result
+ end
+
+ def map_function_name(name, arg_types)
# Note that decorated names can be overridden via def files. Also note that the
# windows api, although using, doesn't have decorated names.
def function_names(name, arg_types)
View
54 lib/ffi/not_implemented.rb
@@ -0,0 +1,54 @@
+#
+# Copyright (C) 2008, 2009 Wayne Meissner
+# Copyright (C) 2009 Luc Heinrich
+# All rights reserved.
+#
+# This file is part of ruby-ffi.
+#
+# This code is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License version 3 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+# version 3 for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# version 3 along with this work. If not, see <http://www.gnu.org/licenses/>.
+
+module FFI
+ class NotImplemented
+ def initialize(cname, required_version, current_version)
+ @cname = cname
+ @required_version = required_version
+ @current_version = current_version
+ end
+
+ #
+ # Raise an error if the function is not available in the current library version
+ #
+ def call(*args)
+ msg = "The function #{@cname} is only available in version #{@required_version} and higher. " +
+ "You are running version #{@current_version}"
+ raise(RuntimeError, msg)
+ end
+
+ #
+ # Attach the invoker to module +mod+ as +mname+
+ #
+ def attach(mod, mname)
+ invoker = self
+ mod.module_eval <<-code
+ @@#{mname} = invoker
+ def self.#{mname}(*args)
+ @@#{mname}.call(*args)
+ end
+ def #{mname}(*args)
+ @@#{mname}.call(*args)
+ end
+ code
+ invoker
+ end
+ end
+end
View
109 lib/ffi/version.rb
@@ -0,0 +1,109 @@
+#
+# Copyright (C) 2008, 2009 Wayne Meissner
+# All rights reserved.
+#
+# This file is part of ruby-ffi.
+#
+# All rights reserved.
+#
+# This code is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License version 3 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+# version 3 for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# version 3 along with this work. If not, see <http://www.gnu.org/licenses/>.
+#
+
+##
+# This is a simplified version of Gem::Version. Besides removing
+# code, this version handles nil differently. A nil version
+# is considered greater than any other version. The reason for
+# this is that when a library has a nil version it should be
+# assumed to contain a function. Thus Versin(nil) > Version('1.2.3').
+
+module FFI
+ class Version
+ include Comparable
+
+ VERSION_PATTERN = '[0-9]+(\.[0-9a-zA-Z]+)*' # :nodoc:
+
+ # A string representation of this Version.
+ attr_reader :version
+ alias to_s version
+
+ # Factory method to create a Version object. Input may be a Version
+ # or a String. Intended to simplify client code.
+ #
+ # ver1 = Version.create('1.3.17') # -> (Version object)
+ # ver2 = Version.create(ver1) # -> (ver1)
+ # ver3 = Version.create(nil) # -> (nil)
+
+ def self.create input
+ if input.respond_to? :version then
+ input
+ else
+ new input
+ end
+ end
+
+ # Constructs a Version from the +version+ string. A version string is a
+ # series of digits or ASCII letters separated by dots.
+ def initialize version
+ @version = version.nil? ? nil : version.to_s.strip
+ end
+
+ # A Version is only eql? to another version if it's specified to the
+ # same precision. Version "1.0" is not the same as version "1".
+ def eql? other
+ self.class === other and @version == other.version
+ end
+
+ def segments
+ # segments is lazy so it can pick up version values that come from
+ # old marshaled versions, which don't go through marshal_load.
+
+ @segments ||= @version.scan(/[0-9]+|[a-z]+/i).map do |s|
+ /^\d+$/ =~ s ? s.to_i : s
+ end
+ end
+
+ # Compares this version with +other+ returning -1, 0, or 1 if the
+ # other version is larger, the same, or smaller than this
+ # one. Attempts to compare to something that's not a
+ # <tt>Gem::Version</tt> return +nil+.
+
+ def <=> other
+ return unless FFI::Version === other
+ return 0 if @version == other.version
+ return 1 if @version.nil?
+ return -1 if other.version.nil?
+
+ lhsegments = segments
+ rhsegments = other.segments
+
+ lhsize = lhsegments.size
+ rhsize = rhsegments.size
+ limit = (lhsize > rhsize ? lhsize : rhsize) - 1
+
+ i = 0
+
+ while i <= limit
+ lhs, rhs = lhsegments[i] || 0, rhsegments[i] || 0
+ i += 1
+
+ next if lhs == rhs
+ return -1 if String === lhs && Numeric === rhs
+ return 1 if Numeric === lhs && String === rhs
+
+ return lhs <=> rhs
+ end
+
+ return 0
+ end
+ end
+end
View
106 spec/ffi/not_implemented_spec.rb
@@ -0,0 +1,106 @@
+#
+# This file is part of ruby-ffi.
+#
+# This code is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License version 3 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+# version 3 for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# version 3 along with this work. If not, see <http://www.gnu.org/licenses/>.
+#
+
+require File.expand_path(File.join(File.dirname(__FILE__), "spec_helper"))
+
+describe FFI::NotImplemented do
+ it 'is initialized with a cname, required version and current version' do
+ FFI::NotImplemented.new('some_fuction', FFI::Version.new(nil), FFI::Version.new(nil))
+ end
+
+ it 'returns a NotImplemented instance' do
+ FFI::NotImplemented.new('some_fuction', FFI::Version.new(nil), FFI::Version.new(nil)).kind_of? FFI::NotImplemented
+ end
+
+ it 'raises an error when invoked' do
+ method = FFI::NotImplemented.new('some_fuction', FFI::Version.new('1.2.3'), FFI::Version.new('1.1.0'))
+ expect do
+ method.call
+ end.to raise_error(RuntimeError, "The function some_fuction is only available in version 1.2.3 and higher. You are running version 1.1.0")
+ end
+
+ it 'can be attached to a module' do
+ module Foo; end
+ fp = FFI::NotImplemented.new('some_fuction', FFI::Version.new('1.2.3'), FFI::Version.new('1.1.0'))
+ fp.attach(Foo, 'add')
+ expect do
+ Foo.add(10, 10).should == 20
+ end.to raise_error(RuntimeError, "The function some_fuction is only available in version 1.2.3 and higher. You are running version 1.1.0")
+ end
+
+ it 'can be used to extend an object' do
+ fp = FFI::NotImplemented.new('some_fuction', FFI::Version.new('1.2.3'), FFI::Version.new('1.1.0'))
+ foo = Object.new
+ class << foo
+ def singleton_class
+ class << self; self; end
+ end
+ end
+ fp.attach(foo.singleton_class, 'add')
+
+ expect do
+ foo.add(10, 10).should == 20
+ end.to raise_error(RuntimeError, "The function some_fuction is only available in version 1.2.3 and higher. You are running version 1.1.0")
+ end
+end
+
+describe "Library has no version" do
+ before do
+ module LibTest
+ extend FFI::Library
+ ffi_lib TestLibrary::PATH
+ end
+ end
+
+ it 'has a nil version' do
+ lib = LibTest.ffi_libraries.first
+ lib.version.should == FFI::Version.new(nil)
+ end
+end
+
+describe "Library version is too old" do
+ before do
+ module LibTest
+ extend FFI::Library
+ ffi_lib TestLibrary::PATH
+ ffi_libraries.first.version = FFI::Version.new('1.1.0')
+ attach_function :testFunctionAdd, [:int, :int, :pointer], :int, :version => '1.2.3'
+ end
+ end
+
+ it 'raises an error when invoked' do
+ expect do
+ function_add = FFI::Function.new(:int, [:int, :int]) { |a, b| a + b }
+ LibTest.testFunctionAdd(10, 10, function_add).should == 20
+ end.to raise_error(RuntimeError, "The function testFunctionAdd is only available in version 1.2.3 and higher. You are running version 1.1.0")
+ end
+end
+
+describe "Library version is good" do
+ before do
+ module LibTest
+ extend FFI::Library
+ ffi_lib TestLibrary::PATH
+ ffi_libraries.first.version = FFI::Version.new('1.2.3')
+ attach_function :testFunctionAdd, [:int, :int, :pointer], :int, :version => '1.1.1'
+ end
+ end
+
+ it 'raises an error when invoked' do
+ function_add = FFI::Function.new(:int, [:int, :int]) { |a, b| a + b }
+ LibTest.testFunctionAdd(10, 10, function_add).should == 20
+ end
+end
View
66 spec/ffi/version_spec.rb
@@ -0,0 +1,66 @@
+##
+## This file is part of ruby-ffi.
+##
+## This code is free software: you can redistribute it and/or modify it under
+## the terms of the GNU Lesser General Public License version 3 only, as
+## published by the Free Software Foundation.
+##
+## This code is distributed in the hope that it will be useful, but WITHOUT
+## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+## FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+## version 3 for more details.
+##
+## You should have received a copy of the GNU Lesser General Public License
+## version 3 along with this work. If not, see <http://www.gnu.org/licenses/>.
+##
+#
+require File.expand_path(File.join(File.dirname(__FILE__), "spec_helper"))
+
+describe "Instantiate a version" do
+ it "saves the correct version" do
+ FFI::Version.new('1.2.3').version.should == '1.2.3'
+ end
+
+ it "saves the correct stripped version" do
+ FFI::Version.new(' 1.2.3 ').version.should == '1.2.3'
+ end
+
+ it "saves nil as empty string" do
+ FFI::Version.new(nil).version.should == nil
+ end
+end
+
+describe "Create a version" do
+ it "returns a new version from a string" do
+ FFI::Version.create('1.2.3').kind_of? FFI::Version
+ end
+
+ it "returns a new version from nil" do
+ FFI::Version.create(nil).kind_of? FFI::Version
+ end
+
+ it "returns the same version from a version" do
+ version = FFI::Version.create('1.2.3')
+ FFI::Version.create(version).should equal(version)
+ end
+end
+
+describe "Versions" do
+ it "equals the same version" do
+ FFI::Version.new('1.2.3').should == FFI::Version.new('1.2.3')
+ end
+
+ it "is less than a nil version" do
+ FFI::Version.new('1.2.3').should < FFI::Version.new(nil)
+ end
+end
+
+describe "Nil version" do
+ it "equals a nil version" do
+ FFI::Version.new(nil).should == FFI::Version.new(nil)
+ end
+
+ it "is greater than a version" do
+ FFI::Version.new(nil).should > FFI::Version.new('1.2.3')
+ end
+end