├── Gemfile ├── lib └── ffi-compiler │ ├── exporter.rb │ ├── version.rb │ ├── fake_ffi │ ├── ffi-compiler │ │ └── loader.rb │ └── ffi.rb │ ├── task.rb │ ├── shell.rb │ ├── platform.rb │ ├── loader.rb │ ├── export_task.rb │ ├── multi_file_task.rb │ └── compile_task.rb ├── Rakefile ├── .gitignore ├── example ├── ext │ ├── example.c │ └── Rakefile ├── Rakefile ├── lib │ └── example │ │ └── example.rb └── example.gemspec ├── ffi-compiler.gemspec ├── Gemfile.lock ├── README.md └── LICENSE /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /lib/ffi-compiler/exporter.rb: -------------------------------------------------------------------------------- 1 | require 'ffi' 2 | 3 | load ARGV[0] 4 | FFI.exporter.dump(ARGV[1]) 5 | -------------------------------------------------------------------------------- /lib/ffi-compiler/version.rb: -------------------------------------------------------------------------------- 1 | 2 | module FFI 3 | module Compiler 4 | VERSION = "1.3.2" 5 | end 6 | end 7 | 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rubygems/tasks' 3 | 4 | Gem::Tasks.new do |t| 5 | t.scm.tag.format = '%s' 6 | end 7 | -------------------------------------------------------------------------------- /lib/ffi-compiler/fake_ffi/ffi-compiler/loader.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | module Compiler 3 | module Loader 4 | def self.find(*args) 5 | end 6 | end 7 | end 8 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/ 2 | .yardoc 3 | *.orig 4 | nbproject/private 5 | pkg 6 | *.orig 7 | *.rej 8 | *.patch 9 | *.diff 10 | build 11 | *.so 12 | *.dylib 13 | *.[oa] 14 | *.gem 15 | core 16 | lib/ffi/types.conf 17 | lib/ffi_c.bundle 18 | lib/ffi_c.so 19 | .idea 20 | *.iml 21 | /example/ext/example.h 22 | -------------------------------------------------------------------------------- /example/ext/example.c: -------------------------------------------------------------------------------- 1 | #include "example.h" 2 | 3 | RBFFI_EXPORT long 4 | example(void) 5 | { 6 | return 0xdeadbeef; 7 | } 8 | 9 | RBFFI_EXPORT int 10 | foo(struct Example_Foo *foo) 11 | { 12 | return 0; 13 | } 14 | 15 | RBFFI_EXPORT int 16 | bar(struct Example_Bar bar, struct Example_Foo *foo) 17 | { 18 | return 0; 19 | } 20 | -------------------------------------------------------------------------------- /lib/ffi-compiler/task.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/tasklib' 3 | require 'rake/clean' 4 | require 'ffi' 5 | require 'tmpdir' 6 | require 'rbconfig' 7 | require_relative 'compile_task' 8 | 9 | module FFI 10 | module Compiler 11 | class Task < CompileTask 12 | warn "#{self} is deprecated" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /example/ext/Rakefile: -------------------------------------------------------------------------------- 1 | require 'ffi-compiler/compile_task' 2 | 3 | FFI::Compiler::CompileTask.new('example') do |t| 4 | t.have_header?('stdio.h', '/usr/local/include') 5 | t.have_func?('puts') 6 | t.have_library?('z') 7 | t.cflags << "-arch x86_64 -arch i386" if t.platform.mac? 8 | t.ldflags << "-arch x86_64 -arch i386" if t.platform.mac? 9 | t.export '../lib/example/example.rb' 10 | end 11 | -------------------------------------------------------------------------------- /example/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rubygems/package_task' 3 | require 'ffi-compiler/export_task' 4 | 5 | def gem_spec 6 | @gem_spec ||= Gem::Specification.load('example.gemspec') 7 | end 8 | 9 | FFI::Compiler::ExportTask.new('lib/example', 'ext', :gem_spec => gem_spec) do |t| 10 | t.export 'example.rb' 11 | end 12 | 13 | Gem::PackageTask.new(gem_spec) do |pkg| 14 | pkg.need_zip = true 15 | pkg.need_tar = true 16 | pkg.package_dir = 'pkg' 17 | end 18 | 19 | -------------------------------------------------------------------------------- /example/lib/example/example.rb: -------------------------------------------------------------------------------- 1 | require 'ffi' 2 | require 'ffi-compiler/loader' 3 | 4 | module Example 5 | extend FFI::Library 6 | ffi_lib FFI::Compiler::Loader.find('example') 7 | 8 | class Foo < FFI::Struct 9 | layout :a, :int, :b, :int 10 | end 11 | 12 | class Bar < FFI::Struct 13 | layout :foo, Foo, :foo_ptr, Foo.by_ref 14 | end 15 | 16 | attach_function :example, [], :long 17 | attach_function :foo, [ Foo ], :int 18 | attach_function :bar, [ Bar.by_value, Foo ], :int 19 | end 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/ffi-compiler/shell.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | require 'rbconfig' 3 | 4 | def shellescape(str) 5 | return str unless str 6 | if FFI::Platform::OS == 'windows' 7 | '"' + str.gsub('"', '""') + '"' 8 | else 9 | str.shellescape 10 | end 11 | end 12 | 13 | def shelljoin(args) 14 | if FFI::Platform::OS == 'windows' 15 | args.reduce { |cmd, arg| cmd + ' ' + shellescape(arg) } 16 | else 17 | args.shelljoin 18 | end 19 | end 20 | 21 | def shellsplit(str) 22 | return str unless str 23 | str.shellsplit 24 | end 25 | -------------------------------------------------------------------------------- /example/example.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'example' 3 | s.version = '0.0.1' 4 | s.author = 'E. Xample' 5 | s.email = 'ffi-example@example.com' 6 | s.homepage = 'http://wiki.github.com/ffi/ffi' 7 | s.summary = 'Ruby FFI example' 8 | s.description = 'Ruby FFI example' 9 | s.files = %w(Rakefile example.gemspec) + Dir.glob("{lib,spec,ext}/**/*") 10 | s.license = 'unknown' 11 | s.required_ruby_version = '>= 1.9.3' 12 | s.extensions << 'ext/Rakefile' 13 | s.add_dependency 'rake' 14 | s.add_dependency 'ffi-compiler', '>= 0.0.2' 15 | s.add_development_dependency 'rspec' 16 | end 17 | -------------------------------------------------------------------------------- /lib/ffi-compiler/platform.rb: -------------------------------------------------------------------------------- 1 | module FFI::Compiler 2 | class Platform 3 | LIBSUFFIX = FFI::Platform.mac? ? 'bundle' : FFI::Platform::LIBSUFFIX 4 | 5 | def self.system 6 | @@system ||= Platform.new 7 | end 8 | 9 | def map_library_name(name) 10 | "#{FFI::Platform::LIBPREFIX}#{name}.#{LIBSUFFIX}" 11 | end 12 | 13 | def arch 14 | FFI::Platform::ARCH 15 | end 16 | 17 | def os 18 | FFI::Platform::OS 19 | end 20 | 21 | def name 22 | FFI::Platform.name 23 | end 24 | 25 | def mac? 26 | FFI::Platform.mac? 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /ffi-compiler.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/ffi-compiler/version' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'ffi-compiler' 5 | s.version = FFI::Compiler::VERSION 6 | s.author = 'Wayne Meissner' 7 | s.email = ['wmeissner@gmail.com', 'steve@advancedcontrol.com.au'] 8 | s.homepage = 'https://github.com/ffi/ffi-compiler' 9 | s.summary = 'Ruby FFI Rakefile generator' 10 | s.description = 'Ruby FFI library' 11 | s.files = %w(ffi-compiler.gemspec Gemfile Rakefile README.md LICENSE) + Dir.glob("{lib,spec}/**/*") 12 | s.license = 'Apache-2.0' 13 | s.required_ruby_version = '>= 1.9' 14 | s.add_dependency 'rake' 15 | s.add_dependency 'ffi', '>= 1.15.5' 16 | s.add_development_dependency 'rspec' 17 | s.add_development_dependency 'rubygems-tasks' 18 | end 19 | 20 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ffi-compiler (1.3.0) 5 | ffi (>= 1.15.5) 6 | rake 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | diff-lcs (1.5.1) 12 | ffi (1.16.3) 13 | io-console (0.7.2) 14 | irb (1.12.0) 15 | rdoc 16 | reline (>= 0.4.2) 17 | psych (5.1.2) 18 | stringio 19 | rake (13.1.0) 20 | rdoc (6.6.2) 21 | psych (>= 4.0.0) 22 | reline (0.4.3) 23 | io-console (~> 0.5) 24 | rspec (3.13.0) 25 | rspec-core (~> 3.13.0) 26 | rspec-expectations (~> 3.13.0) 27 | rspec-mocks (~> 3.13.0) 28 | rspec-core (3.13.0) 29 | rspec-support (~> 3.13.0) 30 | rspec-expectations (3.13.0) 31 | diff-lcs (>= 1.2.0, < 2.0) 32 | rspec-support (~> 3.13.0) 33 | rspec-mocks (3.13.0) 34 | diff-lcs (>= 1.2.0, < 2.0) 35 | rspec-support (~> 3.13.0) 36 | rspec-support (3.13.1) 37 | rubygems-tasks (0.2.6) 38 | irb (~> 1.0) 39 | rake (>= 10.0.0) 40 | stringio (3.1.0) 41 | 42 | PLATFORMS 43 | x86_64-linux 44 | 45 | DEPENDENCIES 46 | ffi-compiler! 47 | rspec 48 | rubygems-tasks 49 | 50 | BUNDLED WITH 51 | 2.5.6 52 | -------------------------------------------------------------------------------- /lib/ffi-compiler/loader.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | require 'ffi' 3 | require_relative 'platform' 4 | 5 | module FFI 6 | module Compiler 7 | module Loader 8 | def self.find(name, start_path = nil) 9 | library = Platform.system.map_library_name(name) 10 | root = false 11 | Pathname.new(start_path || caller_path(caller[0])).ascend do |path| 12 | Dir.glob("#{path}/**/#{FFI::Platform::ARCH}-#{FFI::Platform::OS}/#{library}") do |f| 13 | return f 14 | end 15 | 16 | Dir.glob("#{path}/**/#{library}") do |f| 17 | return f 18 | end 19 | 20 | break if root 21 | 22 | # Next iteration will be the root of the gem if this is the lib/ dir - stop after that 23 | root = File.basename(path) == 'lib' 24 | end 25 | raise LoadError.new("cannot find '#{name}' library") 26 | end 27 | 28 | def self.caller_path(line = caller[0]) 29 | if FFI::Platform::OS == 'windows' 30 | drive = line[0..1] 31 | path = line[2..-1].split(/:/)[0] 32 | full_path = drive + path 33 | else 34 | full_path = line.split(/:/)[0] 35 | end 36 | File.dirname full_path 37 | end 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /lib/ffi-compiler/export_task.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/tasklib' 3 | require 'rake/clean' 4 | 5 | module FFI 6 | module Compiler 7 | class ExportTask < Rake::TaskLib 8 | 9 | def initialize(rb_dir, out_dir, options = {}) 10 | @rb_dir = rb_dir 11 | @out_dir = out_dir 12 | @gem_spec = options[:gem_spec] 13 | @exports = [] 14 | 15 | if block_given? 16 | yield self 17 | define_tasks! 18 | end 19 | end 20 | 21 | def export(rb_file) 22 | @exports << { :rb_file => File.join(@rb_dir, rb_file), :header => File.join(@out_dir, File.basename(rb_file).sub(/\.rb$/, '.h')) } 23 | end 24 | 25 | def export_all 26 | Dir["#@rb_dir/**/*rb"].each do |rb_file| 27 | @exports << { :rb_file => rb_file, :header => File.join(@out_dir, File.basename(rb_file).sub(/\.rb$/, '.h')) } 28 | end 29 | end 30 | 31 | private 32 | def define_tasks! 33 | @exports.each do |e| 34 | file e[:header] => [ e[:rb_file] ] do |t| 35 | ruby "-I#{File.join(File.dirname(__FILE__), 'fake_ffi')} #{File.join(File.dirname(__FILE__), 'exporter.rb')} #{t.prerequisites[0]} #{t.name}" 36 | end 37 | CLEAN.include(e[:header]) 38 | 39 | desc "Export API headers" 40 | task :api_headers => [ e[:header] ] 41 | @gem_spec.files << e[:header] unless @gem_spec.nil? 42 | end 43 | 44 | task :gem => [ :api_headers ] unless @gem_spec.nil? 45 | end 46 | 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/ffi-compiler/multi_file_task.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | module FFI 4 | module Compiler 5 | class MultiFileTask < Rake::MultiTask 6 | def needed? 7 | begin 8 | @application.options.build_all || out_of_date?(File.mtime(name)) 9 | rescue Errno::ENOENT 10 | true 11 | end 12 | end 13 | 14 | def timestamp 15 | begin 16 | File.mtime(name) 17 | rescue Errno::ENOENT 18 | Rake::LATE 19 | end 20 | end 21 | 22 | def invoke_with_call_chain(task_args, invocation_chain) 23 | return unless needed? 24 | super 25 | end 26 | 27 | # This is here for backwards-compatibility so we have namespace-free name 28 | # FileTask (which we used in past) never uses scope 29 | def self.scope_name(scope, task_name) 30 | task_name 31 | end 32 | 33 | private 34 | 35 | def out_of_date?(timestamp) 36 | all_prerequisite_tasks.any? do |prereq| 37 | prereq_task = application[prereq, @scope] 38 | if prereq_task.instance_of?(Rake::FileTask) 39 | File.exist?(prereq_task.name) && prereq_task.timestamp > timestamp 40 | else 41 | prereq_task.needed? 42 | end 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [ffi-compiler](https://github.com/ffi/ffi-compiler) is a ruby library for automating compilation of native libraries for use with [ffi](https://github.com/ffi/ffi) 3 | 4 | To use, define your own ruby->native API using ffi, implement it in C, then use ffi-compiler to compile it. 5 | 6 | Example 7 | ------ 8 | 9 | ###### Directory layout 10 | lib 11 | |- example 12 | |- example.rb 13 | 14 | ext 15 | |- example.c 16 | |- Rakefile 17 | 18 | example.gemspec 19 | 20 | ###### lib/example/example.rb 21 | require 'ffi' 22 | require 'ffi-compiler/loader' 23 | 24 | module Example 25 | extend FFI::Library 26 | ffi_lib FFI::Compiler::Loader.find('example') 27 | 28 | # example function which takes no parameters and returns long 29 | attach_function :example, [], :long 30 | end 31 | 32 | ###### ext/example.c 33 | long 34 | example(void) 35 | { 36 | return 0xdeadbeef; 37 | } 38 | 39 | ###### ext/Rakefile 40 | require 'ffi-compiler/compile_task' 41 | 42 | FFI::Compiler::CompileTask.new('example') do |c| 43 | c.have_header?('stdio.h', '/usr/local/include') 44 | c.have_func?('puts') 45 | c.have_library?('z') 46 | end 47 | 48 | ###### example.gemspec 49 | Gem::Specification.new do |s| 50 | s.extensions << 'ext/Rakefile' 51 | s.name = 'example' 52 | s.version = '0.0.1' 53 | s.email = 'ffi-example' 54 | s.files = %w(example.gemspec) + Dir.glob("{lib,spec,ext}/**/*") 55 | s.add_dependency 'rake' 56 | s.add_dependency 'ffi-compiler' 57 | end 58 | 59 | ###### Build gem and install it 60 | gem build example.gemspec && gem install example-0.0.1.gem 61 | Successfully built RubyGem 62 | Name: example 63 | Version: 0.0.1 64 | File: example-0.0.1.gem 65 | Building native extensions. This could take a while... 66 | Successfully installed example-0.0.1 67 | 68 | ###### Test it 69 | $ irb 70 | 2.0.0dev :001 > require 'example/example' 71 | => true 72 | 2.0.0dev :002 > puts "Example.example=#{Example.example.to_s(16)}" 73 | Example.example=deadbeef 74 | => nil 75 | -------------------------------------------------------------------------------- /lib/ffi-compiler/fake_ffi/ffi.rb: -------------------------------------------------------------------------------- 1 | module FFI 2 | 3 | def self.exporter=(exporter) 4 | @@exporter = exporter 5 | end 6 | 7 | def self.exporter 8 | @@exporter ||= Exporter.new(nil) 9 | end 10 | 11 | class Type 12 | attr_reader :name 13 | def initialize(name) 14 | @name = name 15 | end 16 | end 17 | 18 | class StructByReference < Type 19 | def initialize(struct_class) 20 | super("struct #{struct_class.to_s.gsub('::', '_')} *") 21 | end 22 | end 23 | 24 | class StructByValue < Type 25 | def initialize(struct_class) 26 | super("struct #{struct_class.to_s.gsub('::', '_')}") 27 | end 28 | end 29 | 30 | class CallbackInfo 31 | attr_reader :return_type 32 | attr_reader :arg_types 33 | attr_reader :options 34 | 35 | def initialize(return_type, arg_types = [], *other) 36 | @return_type = return_type 37 | @arg_types = arg_types 38 | @options = options 39 | end 40 | 41 | def name(name) 42 | params = @arg_types.empty? ? 'void' : @arg_types.map(&:name).join(', ') 43 | "#{@return_type.name} (*#{name})(#{params})" 44 | end 45 | end 46 | 47 | PrimitiveTypes = { 48 | :void => 'void', 49 | :bool => 'bool', 50 | :string => 'const char *', 51 | :char => 'char', 52 | :uchar => 'unsigned char', 53 | :short => 'short', 54 | :ushort => 'unsigned short', 55 | :int => 'int', 56 | :uint => 'unsigned int', 57 | :long => 'long', 58 | :ulong => 'unsigned long', 59 | :long_long => 'long long', 60 | :ulong_long => 'unsigned long long', 61 | :float => 'float', 62 | :double => 'double', 63 | :long_double => 'long double', 64 | :pointer => 'void *', 65 | :int8 => 'int8_t', 66 | :uint8 => 'uint8_t', 67 | :int16 => 'int16_t', 68 | :uint16 => 'uint16_t', 69 | :int32 => 'int32_t', 70 | :uint32 => 'uint32_t', 71 | :int64 => 'int64_t', 72 | :uint64 => 'uint64_t', 73 | :buffer_in => 'const in void *', 74 | :buffer_out => 'out void *', 75 | :buffer_inout => 'inout void *', 76 | :varargs => '...' 77 | } 78 | 79 | TypeMap = {} 80 | def self.find_type(type) 81 | return type if type.is_a?(Type) or type.is_a?(CallbackInfo) 82 | 83 | t = TypeMap[type] 84 | return t unless t.nil? 85 | 86 | if PrimitiveTypes.has_key?(type) 87 | return TypeMap[type] = Type.new(PrimitiveTypes[type]) 88 | end 89 | raise TypeError.new("cannot resolve type #{type}") 90 | end 91 | 92 | class Function 93 | def initialize(*args) 94 | end 95 | end 96 | 97 | class Exporter 98 | attr_accessor :mod 99 | attr_reader :functions, :callbacks, :structs 100 | 101 | def initialize(mod) 102 | @mod = mod 103 | @functions = [] 104 | @callbacks = {} 105 | @structs = [] 106 | end 107 | 108 | def attach(mname, fname, result_type, param_types) 109 | @functions << { mname: mname, fname: fname, result_type: result_type, params: param_types.dup } 110 | end 111 | 112 | def struct(name, fields) 113 | @structs << { name: name, fields: fields.dup } 114 | end 115 | 116 | def callback(name, cb) 117 | @callbacks[name] = cb 118 | end 119 | 120 | def dump(out_file) 121 | File.open(out_file, 'w') do |f| 122 | guard = File.basename(out_file).upcase.gsub('.', '_').gsub('/', '_') 123 | f.puts <<-HEADER 124 | #ifndef #{guard} 125 | #define #{guard} 1 126 | 127 | #ifndef RBFFI_EXPORT 128 | # ifdef __cplusplus 129 | # define RBFFI_EXPORT extern "C" 130 | # else 131 | # define RBFFI_EXPORT 132 | # endif 133 | #endif 134 | 135 | HEADER 136 | 137 | @callbacks.each do |name, cb| 138 | f.puts "typedef #{cb.name(name)};" 139 | end 140 | @structs.each do |s| 141 | f.puts "struct #{s[:name].gsub('::', '_')} {" 142 | s[:fields].each do |field| 143 | if field[:type].is_a?(CallbackInfo) 144 | type = field[:type].name(field[:name].to_s) 145 | else 146 | type = "#{field[:type].name} #{field[:name].to_s}" 147 | end 148 | f.puts "#{' ' * 4}#{type};" 149 | end 150 | f.puts '};' 151 | f.puts 152 | end 153 | @functions.each do |fn| 154 | param_string = fn[:params].empty? ? 'void' : fn[:params].map(&:name).join(', ') 155 | f.puts "RBFFI_EXPORT #{fn[:result_type].name} #{fn[:fname]}(#{param_string});" 156 | end 157 | f.puts <<-EPILOG 158 | 159 | #endif /* #{guard} */ 160 | EPILOG 161 | end 162 | end 163 | 164 | end 165 | 166 | module Library 167 | def self.extended(mod) 168 | FFI.exporter.mod = mod 169 | end 170 | 171 | def attach_function(name, func, args, returns = nil, options = nil) 172 | mname, a2, a3, a4, a5 = name, func, args, returns, options 173 | cname, arg_types, ret_type, opts = (a4 && (a2.is_a?(String) || a2.is_a?(Symbol))) ? [ a2, a3, a4, a5 ] : [ mname.to_s, a2, a3, a4 ] 174 | arg_types = arg_types.map { |e| find_type(e) } 175 | FFI.exporter.attach(mname, cname, find_type(ret_type), arg_types) 176 | end 177 | 178 | def ffi_lib(*args) 179 | 180 | end 181 | 182 | def callback(*args) 183 | name, params, ret = if args.length == 3 184 | args 185 | else 186 | [ nil, args[0], args[1] ] 187 | end 188 | native_params = params.map { |e| find_type(e) } 189 | cb = FFI::CallbackInfo.new(find_type(ret), native_params) 190 | FFI.exporter.callback(name, cb) if name 191 | end 192 | 193 | TypeMap = {} 194 | def find_type(type) 195 | t = TypeMap[type] 196 | return t unless t.nil? 197 | 198 | if type.is_a?(Class) && type < Struct 199 | return TypeMap[type] = StructByReference.new(type) 200 | end 201 | 202 | TypeMap[type] = FFI.find_type(type) 203 | end 204 | end 205 | 206 | class Struct 207 | def self.layout(*args) 208 | return if args.size.zero? 209 | fields = [] 210 | if args.first.kind_of?(Hash) 211 | args.first.each do |name, type| 212 | fields << { :name => name, :type => find_type(type), :offset => nil } 213 | end 214 | else 215 | i = 0 216 | while i < args.size 217 | name, type, offset = args[i], args[i+1], nil 218 | i += 2 219 | if args[i].kind_of?(Integer) 220 | offset = args[i] 221 | i += 1 222 | end 223 | fields << { :name => name, :type => find_type(type), :offset => offset } 224 | end 225 | end 226 | FFI.exporter.struct(self.to_s, fields) 227 | end 228 | 229 | def initialize 230 | @data = {} 231 | end 232 | 233 | def [](name) 234 | @data[name] 235 | end 236 | 237 | def []=(name, value) 238 | @data[name] = value 239 | end 240 | 241 | def self.callback(params, ret) 242 | FFI::CallbackInfo.new(find_type(ret), params.map { |e| find_type(e) }) 243 | end 244 | 245 | TypeMap = {} 246 | def self.find_type(type) 247 | t = TypeMap[type] 248 | return t unless t.nil? 249 | 250 | if type.is_a?(Class) && type < Struct 251 | return TypeMap[type] = StructByValue.new(type) 252 | end 253 | 254 | TypeMap[type] = FFI.find_type(type) 255 | end 256 | 257 | def self.in 258 | ptr(:in) 259 | end 260 | 261 | def self.out 262 | ptr(:out) 263 | end 264 | 265 | def self.ptr(flags = :inout) 266 | StructByReference.new(self) 267 | end 268 | 269 | def self.val 270 | StructByValue.new(self) 271 | end 272 | 273 | def self.by_value 274 | self.val 275 | end 276 | 277 | def self.by_ref(flags = :inout) 278 | self.ptr(flags) 279 | end 280 | 281 | end 282 | end -------------------------------------------------------------------------------- /lib/ffi-compiler/compile_task.rb: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/tasklib' 3 | require 'rake/clean' 4 | require 'ffi' 5 | require 'tmpdir' 6 | require 'rbconfig' 7 | require_relative 'platform' 8 | require_relative 'shell' 9 | require_relative 'multi_file_task' 10 | 11 | module FFI 12 | module Compiler 13 | DEFAULT_CFLAGS = %w(-fexceptions -O -fno-omit-frame-pointer -fno-strict-aliasing) 14 | DEFAULT_LDFLAGS = %w(-fexceptions) 15 | 16 | class Flags 17 | attr_accessor :raw 18 | 19 | def initialize(flags) 20 | @flags = flags 21 | @raw = true # For backward compatibility 22 | end 23 | 24 | def <<(flag) 25 | if @raw 26 | @flags += shellsplit(flag.to_s) 27 | else 28 | @flags << flag 29 | end 30 | end 31 | 32 | def to_a 33 | @flags 34 | end 35 | 36 | def to_s 37 | shelljoin(@flags) 38 | end 39 | end 40 | 41 | class CompileTask < Rake::TaskLib 42 | attr_reader :cflags, :cxxflags, :ldflags, :libs, :platform 43 | attr_accessor :name, :ext_dir, :source_dirs, :exclude 44 | 45 | def initialize(name) 46 | @name = File.basename(name) 47 | @ext_dir = File.dirname(name) 48 | @source_dirs = [@ext_dir] 49 | @exclude = [] 50 | @defines = [] 51 | @include_paths = [] 52 | @library_paths = [] 53 | @libraries = [] 54 | @headers = [] 55 | @functions = [] 56 | @cflags = Flags.new(shellsplit(ENV['CFLAGS']) || DEFAULT_CFLAGS.dup) 57 | @cxxflags = Flags.new(shellsplit(ENV['CXXFLAGS']) || DEFAULT_CFLAGS.dup) 58 | @ldflags = Flags.new(shellsplit(ENV['LDFLAGS']) || DEFAULT_LDFLAGS.dup) 59 | @libs = [] 60 | @platform = Platform.system 61 | @exports = [] 62 | 63 | yield self if block_given? 64 | define_task! 65 | end 66 | 67 | def add_include_path(path) 68 | @include_paths << path 69 | end 70 | 71 | def add_define(name, value=1) 72 | @defines << "-D#{name}=#{value}" 73 | end 74 | 75 | def have_func?(func) 76 | main = <<-C_FILE 77 | extern void #{func}(); 78 | int main(int argc, char **argv) { #{func}(); return 0; } 79 | C_FILE 80 | 81 | if try_compile(main) 82 | @functions << func 83 | return true 84 | end 85 | false 86 | end 87 | 88 | def have_header?(header, *paths) 89 | try_header(header, @include_paths) || try_header(header, paths) 90 | end 91 | 92 | def have_library?(libname, *paths) 93 | try_library(libname, paths: @library_paths) || try_library(libname, paths: paths) 94 | end 95 | 96 | def have_library(lib, func = nil, headers = nil, &b) 97 | try_library(lib, function: func, headers: headers, paths: @library_paths) 98 | end 99 | 100 | def find_library(lib, func, *paths) 101 | try_library(lib, function: func, paths: @library_paths) || try_library(libname, function: func, paths: paths) 102 | end 103 | 104 | def export(rb_file) 105 | @exports << { :rb_file => rb_file, :header => File.join(@ext_dir, File.basename(rb_file).sub(/\.rb$/, '.h')) } 106 | end 107 | 108 | private 109 | def define_task! 110 | pic_flags = %w(-fPIC) 111 | so_flags = [] 112 | 113 | if @platform.mac? 114 | pic_flags = [] 115 | so_flags << '-bundle' 116 | 117 | elsif @platform.name =~ /linux/ 118 | so_flags << "-shared -Wl,-soname,#{lib_name}" 119 | 120 | else 121 | so_flags << '-shared' 122 | end 123 | so_flags = shelljoin(so_flags) 124 | 125 | out_dir = "#{@platform.arch}-#{@platform.os}" 126 | if @ext_dir != '.' 127 | out_dir = File.join(@ext_dir, out_dir) 128 | end 129 | 130 | directory(out_dir) 131 | CLOBBER.include(out_dir) 132 | 133 | lib_name = File.join(out_dir, Platform.system.map_library_name(@name)) 134 | 135 | iflags = @include_paths.uniq.map { |p| "-I#{p}" } 136 | @defines += @functions.uniq.map { |f| "-DHAVE_#{f.upcase}=1" } 137 | @defines += @headers.uniq.map { |h| "-DHAVE_#{h.upcase.sub(/\./, '_')}=1" } 138 | 139 | cflags = shelljoin(@cflags.to_a + pic_flags + iflags + @defines) 140 | cxxflags = shelljoin(@cxxflags.to_a + @cflags.to_a + pic_flags + iflags + @defines) 141 | ld_flags = shelljoin(@library_paths.map { |path| "-L#{path}" } + @ldflags.to_a) 142 | libs = shelljoin(@libraries.map { |l| "-l#{l}" } + @libs) 143 | 144 | src_files = [] 145 | obj_files = [] 146 | @source_dirs.each do |dir| 147 | files = FileList["#{dir}/**/*.{c,cpp,m}"] 148 | unless @exclude.empty? 149 | files.delete_if { |f| f =~ Regexp.union(*@exclude) } 150 | end 151 | src_files += files 152 | obj_files += files.ext('.o').map { |f| File.join(out_dir, f.sub(/^#{dir}\//, '')) } 153 | end 154 | 155 | index = 0 156 | src_files.each do |src| 157 | obj_file = obj_files[index] 158 | if src =~ /\.[cm]$/ 159 | file obj_file => [ src, File.dirname(obj_file) ] do |t| 160 | sh "#{cc} #{cflags} -o #{shellescape(t.name)} -c #{shellescape(t.prerequisites[0])}" 161 | end 162 | 163 | else 164 | file obj_file => [ src, File.dirname(obj_file) ] do |t| 165 | sh "#{cxx} #{cxxflags} -o #{shellescape(t.name)} -c #{shellescape(t.prerequisites[0])}" 166 | end 167 | end 168 | 169 | CLEAN.include(obj_file) 170 | index += 1 171 | end 172 | 173 | ld = src_files.detect { |f| f =~ /\.cpp$/ } ? cxx : cc 174 | 175 | # create all the directories for the output files 176 | obj_files.map { |f| File.dirname(f) }.sort.uniq.map { |d| directory d } 177 | 178 | desc "Build dynamic library" 179 | MultiFileTask.define_task(lib_name => src_files + obj_files) do |t| 180 | objs = t.prerequisites.select { |file| file.end_with?('.o') } 181 | sh "#{ld} #{so_flags} -o #{shellescape(t.name)} #{shelljoin(objs)} #{ld_flags} #{libs}" 182 | end 183 | CLEAN.include(lib_name) 184 | 185 | @exports.each do |e| 186 | desc "Export #{e[:rb_file]}" 187 | file e[:header] => [ e[:rb_file] ] do |t| 188 | ruby "-I#{File.join(File.dirname(__FILE__), 'fake_ffi')} -I#{File.dirname(t.prerequisites[0])} #{File.join(File.dirname(__FILE__), 'exporter.rb')} #{shellescape(t.prerequisites[0])} #{shellescape(t.name)}" 189 | end 190 | 191 | obj_files.each { |o| file o => [ e[:header] ] } 192 | CLEAN.include(e[:header]) 193 | 194 | desc "Export API headers" 195 | task :api_headers => [ e[:header] ] 196 | end 197 | 198 | task :default => [ lib_name ] 199 | task :package => [ :api_headers ] 200 | end 201 | 202 | def try_header(header, paths) 203 | main = <<-C_FILE 204 | #include <#{header}> 205 | int main(int argc, char **argv) { return 0; } 206 | C_FILE 207 | 208 | if paths.empty? && try_compile(main) 209 | @headers << header 210 | return true 211 | end 212 | 213 | paths.each do |path| 214 | if try_compile(main, "-I#{path}") 215 | @include_paths << path 216 | @headers << header 217 | return true 218 | end 219 | end 220 | false 221 | end 222 | 223 | 224 | def try_library(libname, options = {}) 225 | func = options[:function] || 'main' 226 | paths = options[:paths] || '' 227 | main = <<-C_FILE 228 | #{(options[:headers] || []).map {|h| "#include <#{h}>"}.join('\n')} 229 | extern int #{func}(); 230 | int main() { return #{func}(); } 231 | C_FILE 232 | 233 | if paths.empty? && try_compile(main) 234 | @libraries << libname 235 | return true 236 | end 237 | 238 | paths.each do |path| 239 | if try_compile(main, "-L#{path}", "-l#{libname}") 240 | @library_paths << path 241 | @libraries << libname 242 | end 243 | end 244 | end 245 | 246 | def try_compile(src, *opts) 247 | Dir.mktmpdir do |dir| 248 | path = File.join(dir, 'ffi-test.c') 249 | File.open(path, 'w') do |f| 250 | f << src 251 | end 252 | cflags = shelljoin(opts) 253 | output = File.join(dir, 'ffi-test') 254 | begin 255 | return system "#{cc} #{cflags} -o #{shellescape(output)} -c #{shellescape(path)} > #{shellescape(path)}.log 2>&1" 256 | rescue 257 | return false 258 | end 259 | end 260 | end 261 | 262 | def cc 263 | @cc ||= (ENV['CC'] || RbConfig::CONFIG['CC'] || 'cc') 264 | end 265 | 266 | def cxx 267 | @cxx ||= (ENV['CXX'] || RbConfig::CONFIG['CXX'] || 'c++') 268 | end 269 | end 270 | end 271 | end 272 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2020 FFI 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------