【问题标题】:Native extensions fallback to pure Ruby if not supported on gem install如果 gem 安装不支持本机扩展回退到纯 Ruby
【发布时间】:2013-06-28 16:17:42
【问题描述】:

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

我想在单个 gem 中维护该功能的 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/1479http://rubyforge.org/pipermail/rubygems-developers/2007-November/003220.html 暗示 Ruby gem 并不真正支持此功能。不过最近没有什么,所以我希望 SO 上的某个人有一些最新的知识?

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

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

【问题讨论】:

  • 两个宝石都可以,例如json gem 有两种变体:json(带有 C 扩展名)和json_pure(纯 Ruby)。
  • @Stefan:在我链接的对话中,jsonjson_pure 的作者/维护者显然更喜欢它。除了发布 gem 的两个变体的额外工作外,依赖项目本身可能是其他东西的依赖项,最终不得不使用最低公分母,或者还必须提供两个变体来管理他们不编码的依赖项.我不会称其为“很好”,但如果这是最好的,那我也能做到
  • @Stefan:OP中体现的Neil的设计意图肯定是对的。他的问题很好也很重要,我自己对答案很感兴趣,请点赞。
  • 这个我也很感兴趣,这个问题真的很重要。

标签: ruby gem ruby-c-extension


【解决方案1】:

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

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

这对我有用:

  1. extconf.rb 中使用mkmf#have_* methods 检查您需要的一切。然后拨打#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,你需要在 before 打包之前适配 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 除外)

【讨论】:

    【解决方案2】:

    这是我迄今为止尝试回答我自己的问题的最佳结果。它似乎适用于 JRuby(在 Travis 和我在 RVM 下的本地安装中测试),这是我的主要目标。但是,我很想确认它在其他环境中的工作情况,以及关于如何使其更通用和/或更健壮的任何意见:


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

    ext/foo/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...,并且没有任何迹象表明扩展编译已被跳过。但是,如果您使用 gem install foo --verbose 调用安装程序,您确实会看到添加到 extconf.rb 的消息,所以还不错。

    【讨论】:

    • 如果您可以将编译过程(使用Makefile)放入begin rescue end 块中会很酷。在我的情况下,我不想将扩展作为单独的 gem,如果由于某种原因编译失败,gem 应该回退到使用 Ruby 实现,假设我们在运行 Makefile 之前无法确定它会成功。
    【解决方案3】:

    这是一个想法,基于来自 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”上传递的参数等。

    【讨论】:

    • extconf.rb 中的代码无效。但是 gemspec 中围绕对 rake-install 的依赖和声明扩展的类似代码似乎可以按要求工作。我需要多玩一点,但它很有希望
    • 编辑 gemspec 允许我再次在 JRuby 上测试 gem,我的所有 rake 任务都在所有测试 rubies 中工作,但最终它也没有工作。问题是.gemspec 处理得太快了,所以只有当gem 的构建在同一个Ruby 目标中完成时才有效。但是 extconf.rb 处理得太晚了,如果 gem 包含 extconf.rb 目标系统可以在它的任何逻辑运行之前拒绝它。
    • 感谢您的指点,他们得出了我认为可行的答案。但是,除了检测可以编译/不编译的目标之外,还有更多内容,因此将我的发现添加为新答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多