如果gem安装不支持原生扩展,则回退到纯Ruby

我正在开发一个gem,它目前是纯Ruby,但我也一直在为其中一个function开发一个更快的C变种。 该function在纯Ruby中可用,但有时很慢。 缓慢只会影响一些潜在用户(取决于他们需要哪些function,以及他们如何使用它们),因此如果无法在目标系统上编译,那么让gem可以优雅地回退到仅使用Ruby的函数是有意义的。

我想在一个gem中维护该function的Ruby和C变体,并在安装时提供gem的最佳(即最快)体验。 这将使我能够从我的一个项目中支持最广泛的潜在用户。 它还允许其他人的依赖gem和项目使用目标系统上最好的可用依赖性,而不是兼容性最低的通用分母版本。

我希望在运行时回退的require出现在主lib/foo.rb文件中,就像这样:

 begin require 'foo/foo_extended' rescue LoadError require 'foo/ext_bits_as_pure_ruby' end 

但是,我不知道如何让gem安装检查(或尝试和失败)本机扩展支持,以便gem安装正确,无论它是否可以构建’foo_extended’。 当我研究如何做到这一点时,我主要发现了几年前的讨论,例如http://permalink.gmane.org/gmane.comp.lang.ruby.gems.devel/1479和http://rubyforge.org/ pipermail / ruby​​gems-developers / 2007-November / 003220.html暗示Rubygem并不真正支持这一function。 没什么新鲜的,所以我希望SO上有人有一些更新的知识?

我的理想解决方案是在尝试构建扩展之前检测目标Ruby不支持(或者可能根本不希望在项目级别)C本机扩展的方法。 但是,如果不是太脏,尝试/捕获机制也可以。

这有可能,如果是这样的话怎么样? 或者建议发布两个gem变体(例如foofoo_ruby ),我在搜索时发现,仍然是目前的最佳实践?

这是一个基于http://guides.rubygems.org/c-extensions/和http://yorickpeterse.com/articles/hacking-extconf-rb/的信息 。

看起来你可以将逻辑放在extconf.rb中。 例如,查询RUBY_DESCRIPTION常量并确定您是否在支持本机扩展的Ruby中:

 $ irb jruby-1.6.8 :001 > RUBY_DESCRIPTION => "jruby 1.6.8 (ruby-1.8.7-p357) (2012-09-18 1772b40) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_51) [darwin-x86_64-java]" 

所以你可以尝试在extconf.rb中将条件包装在条件中(在extconf.rb中):

 unless RUBY_DESCRIPTION =~ /jruby/ do require 'mkmf' # stuff create_makefile('my_extension/my_extension') end 

显然,你会想要更复杂的逻辑,抓住传递“gem install”的参数等。

这是我迄今为止试图回答我自己的问题的最佳结果。 它似乎适用于JRuby(在Travis和我在RVM下的本地安装中测试),这是我的主要目标。 但是,我会对在其他环境中工作的确认非常感兴趣,并且对于如何使其更通用和/或更健壮的任何输入:


gem安装代码需要一个Makefile作为extconf.rb输出,但是对于应该包含的内容没有意见。 因此, extconf.rb可以决定创建一个do nothing Makefile ,而不是从mkmf调用mkmf 。 在实践中可能看起来像这样:

EXT /富/ extconf.rb

 can_compile_extensions = false want_extensions = true begin require 'mkmf' can_compile_extensions = true rescue Exception # This will appear only in verbose mode. $stderr.puts "Could not require 'mkmf'. Not fatal, the extensions are optional." end if can_compile_extensions && want_extensions create_makefile( 'foo/foo' ) else # Create a dummy Makefile, to satisfy Gem::Installer#install mfile = open("Makefile", "wb") mfile.puts '.PHONY: install' mfile.puts 'install:' mfile.puts "\t" + '@echo "Extensions not installed, falling back to pure Ruby version."' mfile.close end 

正如问题所示,这个答案还需要以下逻辑来加载主库中的Ruby回退代码:

lib / foo.rb(摘录)

 begin # Extension target, might not exist on some installations require 'foo/foo' rescue LoadError # Pure Ruby fallback, should cover all methods that are otherwise in extension require 'foo/foo_pure_ruby' end 

遵循这条路线还需要一些rake任务,所以默认的rake任务不会尝试编译我们正在测试的Rubies,它们没有编译扩展的能力:

Rakefile(摘录)

 def can_compile_extensions return false if RUBY_DESCRIPTION =~ /jruby/ return true end if can_compile_extensions task :default => [:compile, :test] else task :default => [:test] end 

注意Rakefile部分不必是完全通用的,它只需要覆盖我们想要在本地构建和测试gem的已知环境(例如所有Travis目标)。

我注意到一个烦恼。 默认情况下,您将看到Ruby Gems的消息Building native extensions. This could take a while... Building native extensions. This could take a while... ,并且没有迹象表明已跳过扩展编译。 但是,如果你使用gem install foo --verbose调用安装程序gem install foo --verbose你会看到添加到extconf.rb的消息,所以它并不是太糟糕。

https://stackoverflow.com/posts/50886432/edit

我尝试了其他答案,无法让他们建立在最近的ruby上。

这对我有用:

  1. extconf.rb使用mkmf#have_*方法检查所需的一切。 然后调用#create_makefile ,无论如何。
  2. 使用#have_*生成的预处理器常量来跳过C文件中的内容。
  3. 检查Ruby中定义的方法/模块。
  4. 如果你想支持JRuby等,你需要一个更复杂的发布设置。

一个简单的例子,如果缺少某些东西,将跳过整个C扩展:

1. ext/my_gem/extconf.rb

 require 'mkmf' have_struct_member('struct foo', 'bar') create_makefile('my_gem/my_gem') 

2. ext/my_gem/my_gem.c

 #ifndef HAVE_STRUCT_FOO_BAR // C ext cant be compiled, ignore because it's optional void Init_my_gem() {} #else #include "ruby.h" void Init_my_gem() { VALUE mod; mod = rb_define_module("MyGemExt"); // attach methods to module } #endif 

3. lib/my_gem.rb

 class MyGem begin require 'my_gem/my_gem' include MyGemExt rescue LoadError, NameError warn 'Running MyGem without C extension, using slower Ruby fallback' include MyGem::RubyFallback end end 

4.如果要为JRuby发布gem,则需要在打包调整gemspec。 这将允许您构建和发布多个版本的gem。 我能想到的最简单的解决方案:

Rakefile

 require 'rubygems/package_task' namespace :java do java_gemspec = eval File.read('./my_gem.gemspec') java_gemspec.platform = 'java' java_gemspec.extensions = [] # override to remove C extension Gem::PackageTask.new(java_gemspec) do |pkg| pkg.need_zip = true pkg.need_tar = true pkg.package_dir = 'pkg' end end task package: 'java:gem' 

然后运行$ rake package && gem push pkg/my_gem-0.1.0 && gem push pkg/my_gem-0.1.0-java以发布新版本。

如果您只想在JRuby上运行,而不是为它分发gem,这就足够了(但是,它不适用于释放gem,因为它在打包之前进行了评估):

my_gem.gemspec

 if RUBY_PLATFORM !~ /java/i s.extensions = %w[ext/my_gem/extconf.rb] end 

这种方法有两个优点:

  • create_makefile应该适用于每个环境
  • compile任务可以保留在其他任务之前(JRuby除外)