Native extensions fallback to pure Ruby if not supported on gem install

后端 未结 3 1778
走了就别回头了
走了就别回头了 2021-02-04 10:38

I am developing a gem, which is currently pure Ruby, but I have also been developing a faster C variant for one of the features. The feature is usable, but sometimes slow, in pu

3条回答
  •  不思量自难忘°
    2021-02-04 11:24

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

    I tried the other answers and could not get them to build on recent Rubies.

    This worked for me:

    1. Use mkmf#have_* methods in extconf.rb to check for everything you need. Then call #create_makefile, no matter what.
    2. Use the preprocessor constants generated by #have_* to skip things in your C file.
    3. Check which methods/modules are defined in Ruby.
    4. If you want to support JRuby et al, you'll need a more complex release setup.

    A simple example where the whole C extension is skipped if something is missing:

    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. If you want to release the gem for JRuby, you need to adapt the gemspec before packaging. This will allow you to build and release multiple versions of the gem. The simplest solution I can think of:

    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'
    

    Then run $ rake package && gem push pkg/my_gem-0.1.0 && gem push pkg/my_gem-0.1.0-java to release a new version.

    If you just want to run on JRuby, not distribute the gem for it, this will suffice (it will not work for releasing the gem, though, as it is evaluated before packaging):

    my_gem.gemspec

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

    This approach has two advantages:

    • create_makefile should work in every environment
    • a compile task can remain prepended to other tasks (except on JRuby)

提交回复
热议问题