12

我正在开发一个 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 gems 并不真正支持此功能。不过最近没有什么,所以我希望 SO 上的某个人有一些最新的知识?

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

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

4

3 回答 3

4

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


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

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 任务不会尝试在我们正在测试的没有能力编译扩展的红宝石上编译:

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,所以还不错。

于 2013-07-15T11:52:44.567 回答
2

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

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

这对我有用:

  1. 使用mkmf#have_*方法extconf.rb检查你需要的一切。然后打电话#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 除外)
于 2018-06-16T08:57:09.083 回答
1

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

于 2013-07-12T17:54:35.510 回答