ruby-vips icon indicating copy to clipboard operation
ruby-vips copied to clipboard

Ensure compatibility with a single shared libvips library

Open kleisauke opened this issue 1 year ago • 5 comments

See: #372.

kleisauke avatar Mar 04 '24 14:03 kleisauke

Hiya,

Hmmm I'm a little unsure about this approach, it means we have the library name hidden in the library_name method, when it's supposed to be a parameter.

Instead, how adding an new method for detecting a semi-static libvips? Something like (untested):

def library_name(name, abi_number)
  if FFI::Platform.windows?
    "lib#{name}-#{abi_number}.dll"
  elsif FFI::Platform.mac?
    "#{name}.#{abi_number}"
  else
    "#{name}.so.#{abi_number}"
  end
end

module Vips
  extend FFI::Library

  ffi_lib library_name("vips", 42)

  # see if we can get glib functions from the libvips library
  #
  # if we can, we are dealing with a semi-static libvips binary or a platform 
  # which handles library dependencies automatically, and we can
  # fetch everything from that ... if it fails, we will need to open separate
  # glib and gobject libraries
  begin
    attach_function :g_malloc, [:size_t], :pointer
    is_semistatic = true
  rescue => e
    is_semistatic = false
  end
  
  def semistatic?
    is_semistatic
  end
end

module GLib
  class << self
    attr_accessor :logger
  end
  @logger = Logger.new($stdout)
  @logger.level = Logger::WARN

  extend FFI::Library

  if Vips::semistatic?
    ffi_lib library_name("glib-2.0", 0)
  else
    ffi_lib library_name("vips", 42)
  end

...

Hmm or something like that anyway. I've not thought about this very carefully!

pyvips could maybe use something similar, I think at the moment it'll try to load glib as a test for semistatic, but that could break on windows if the user has glib from another project on their PATH. Seeing if we can get g_free or whatever from libvips ought to be safer.

Or I've badly misunderstood!

jcupitt avatar Mar 04 '24 17:03 jcupitt

Hmm semistatic? is a bad name, it should maybe be separate_libs?. Anyway, it controls whether we need to open libraries separately.

jcupitt avatar Mar 04 '24 17:03 jcupitt

It looks like this can be simplified by passing an array of library names to ffi_lib, see: https://github.com/ffi/ffi/wiki/Using-Multiple-and-Alternate-Libraries#using-one-of-three-alternate-libraries

I just tested this on Windows, both the -static and -static-ffi binaries seems to work! :tada:

PS> git clone -b single-shared-vips-compat https://github.com/kleisauke/ruby-vips.git
PS> cd ruby-vips
PS> gem build ruby-vips.gemspec
PS> gem install ruby-vips-2.2.1.gem ffi --ignore-dependencies
PS> $env:RUBY_DLL_PATH = "C:\vips-dev-8.15\bin";
PS> irb
irb(main):001:0> require "vips"
=> true
irb(main):002:0> im = Vips::Image.black 100, 100
=> #<Image 100x100 uchar, 1 bands, multiband>

kleisauke avatar Mar 04 '24 18:03 kleisauke

pyvips could maybe use something similar, I think at the moment it'll try to load glib as a test for semistatic, but that could break on windows if the user has glib from another project on their PATH. Seeing if we can get g_free or whatever from libvips ought to be safer.

Ah, that would indeed be problematic, but only for Windows, so maybe we shouldn't spend too much time on this.

Wrapping all GLib and GObject symbols would indeed solve this (this was suggested in https://github.com/libvips/libvips/discussions/2788), but it's a lot of work, and I'm not sure if it's worth it. :(

kleisauke avatar Mar 04 '24 18:03 kleisauke

I restarted the flaky tests. This is ready for review now.

kleisauke avatar Mar 04 '24 18:03 kleisauke

Nice! I didn't know you could pass sets of library names to ffi_lib, that's great.

But .... wouldn't it still be better to try to fetch g_*() funcs from libvips? I think it's not too hard, I tried:

https://github.com/libvips/ruby-vips/commit/2c55d8bb5e095c9c7a4b20e36841350412c653f8

And it seems to work, though I've not checked windows.

jcupitt avatar Mar 05 '24 12:03 jcupitt

Ah, I see what you meant. Indeed, that would work. It also fixes the PATH issue described above, nice!

kleisauke avatar Mar 05 '24 13:03 kleisauke

I merged commit 2c55d8bb5e095c9c7a4b20e36841350412c653f8 within this branch, additionally I had to apply commit 491d7d9d176f6baaa656d6126b358daf5b27d124 to fix this error on Windows:

irb(main):001:0>  require "vips"
C:/Ruby32-x64/lib/ruby/gems/3.2.0/gems/ffi-1.16.3-x64-mingw-ucrt/lib/ffi/library.rb:216:in `attach_function': Function 'g_malloc' not found in [libvips-42.dll] (FFI::NotFoundError)
        from C:/Ruby32-x64/lib/ruby/gems/3.2.0/gems/ruby-vips-2.2.1/lib/vips.rb:49:in `<module:Vips>'
        from C:/Ruby32-x64/lib/ruby/gems/3.2.0/gems/ruby-vips-2.2.1/lib/vips.rb:43:in `<top (required)>'
        from <internal:C:/Ruby32-x64/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:160:in `require'
        from <internal:C:/Ruby32-x64/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:160:in `rescue in require'
        from <internal:C:/Ruby32-x64/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:40:in `require'
        from (irb):1:in `<main>'
        from C:/Ruby32-x64/lib/ruby/gems/3.2.0/gems/irb-1.6.2/exe/irb:11:in `<top (required)>'
        from C:/Ruby32-x64/bin/irb:33:in `load'
        from C:/Ruby32-x64/bin/irb:33:in `<main>'
<internal:C:/Ruby32-x64/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:86:in `require': cannot load such file -- vips (LoadError)
        from <internal:C:/Ruby32-x64/lib/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:86:in `require'
        from (irb):1:in `<main>'
        from C:/Ruby32-x64/lib/ruby/gems/3.2.0/gems/irb-1.6.2/exe/irb:11:in `<top (required)>'
        from C:/Ruby32-x64/bin/irb:33:in `load'
        from C:/Ruby32-x64/bin/irb:33:in `<main>'

It seems to work properly now.

kleisauke avatar Mar 07 '24 09:03 kleisauke

Great! Thank you for fixing this Kleis.

jcupitt avatar Mar 07 '24 10:03 jcupitt

I really like ruby, but I still get confused about when to use . and :: :(

jcupitt avatar Mar 07 '24 10:03 jcupitt

I really like ruby, but I still get confused about when to use . and :: :(

It's like a matter of personal preference.

Though for example I had an issue with :: recently. In my code that utilizes AWS sdk there was a line of code:

Aws::Sigv4::Signer.new(

Then for the same program testing I used a poor-man's stubbing for third-party libraries like this:

Aws = Struct.new(:Sigv4).new(Struct.new(:new).new(...

and it obviously failed because Struct does not respond to ::. But it works once you rewrite it as:

Aws.Sigv4.Signer.new(

My current choice is to use . everywhere and use the leading :: to access the global modules. I.e.:

::Aws.Sigv4.Signer.new(

and the same for ::File, ::Dir, ::Struct, ::ENV, etc. (It only sucks you can't do ::Integer(iteger_string))

Nakilon avatar Mar 07 '24 20:03 Nakilon