What is the best way to load DLL with Fiddle?

Hello !

In the last version of my extension, OpenCutList, I bundled some features in an external DLL. And I use Fiddle to load it.

It works fine on most computer, but it failed to load if the system username contains non-ascii characters.

As you know SketchUp extensions are installed in user AppData. And this folder is child of a folder that contains the username. And a lot of users have non-ascii characters in their name…

I use Fiddle::Importer.dlload to load DLL. But internally this method doesn’t accept UTF-8 string for path :frowning:

Then what could be the best way to load an external DLL (or dylib) placed in our extension folder through Fiddle ?

I’ve had a directory with non ACSII characters that I’ve moved from desktop to desktop, and the following works with SU 2017. I grabbed the Clippy.dll file from the last GHA artifact…

    def load_ddl
      # __dir__ contains 'uby киї_テスト' (spaces & non ACSII characters)
      path = "#{__dir__}/Clippy.dll"
      puts "#{path.encoding}  #{path}"
      handle = Fiddle.dlopen path
      dlload handle
      set_extern  # methods from ruby/lib/clippy/clippy.rb _load_lib
    end

So, it may be an encoding issue, not sure. I used to have a user account with similar characters, but haven’t yet created one on the current desktop. The issue may not be related to the user account ‘string’, but to the ‘system’ character set.

Also, this might be a Window 10 issue, as I thought Windows 11 made some encoding changes. JFYI, also tried it with several stand-alone Ruby versions, and it worked fine.

Hi @MSP_Greg , thank you for your investigations.

As far as I can test this code failed on Win 10 with SU 2023.

Files :

  • c:\temp\uby киї_テスト\Clippy.dll
  • c:\temp\uby киї_テスト\loader.rb
require 'fiddle'
def load_lib
  dir = __dir__
  dir = dir.force_encoding("UTF-8") if dir.respond_to?(:force_encoding)
  path = "#{dir}/Clippy.dll"
  puts "File.exist? = #{File.exist?(path)}"
  puts "#{path.encoding} #{path}"
  puts handle = Fiddle.dlopen(path)
  puts handle.sym("c_version") unless handle.nil?
end

Returns :

File.exist? = true
UTF-8 C:/temp/uby киї_テスト/Clippy.dll
Error: #<Fiddle::DLError: No such file or directory>

Yes, of course, the problem occurs even if extension is loaded from other non-ascii path. But I mention the system user name because in AppData path (where SU extension are natively installed) the only folder that can be non-ascii is named by the user name.

Unfortunately this seems to occur on Windows 11 too.

This appears to work:

# encoding: UTF-8

require 'fiddle'

def load_lib(lib, path = __dir__)
  puts "#{path.encoding} #{path.inspect}"
  puts "Path exist? = #{File.exist?(path)}"
  #
  handle = nil
  Dir.chdir(path) do
    handle = Fiddle.dlopen(lib)
  end
  if handle.nil?
    puts "handle is nil"
  else
    puts handle.inspect
  end
  return handle
end

Call like:

handle = load_lib("Clippy.dll")
puts handle.sym("c_version") unless handle.nil?

Hi @DanRathbun,

Thank you for your investigation too!

We try the solution that change the working directory 2 days ago. And after several tests sometime it works sometimes it doesn’t. Both on our Win11 and MacOS dev machines.
I don’t know why … is there dlopen cache or something like that ?

But what we try was :

Dir.chdir(path)
handle = Fiddle.dlopen(lib)

and not :

Dir.ch

Dir.chdir(path) do
    handle = Fiddle.dlopen(lib)
end

Never change the working directory without changing it back. The block form of Dir::chdir does this for you.

NO idea. I’m not a Fiddle nor C expert. You’d need to dive into the Fiddle C source.

Yes we did.

wd = Dir.getwd
Dir.chdir(path)
handle = Fiddle.dlopen(lib)
Dir.chdir(wd)

As far a I know, dlopen is not specific to Fiddle.

Fiddle.dlopen is just a convenience wrapper method around the Fiddle::Handle.new constructor.

I think you meant C’s dlopen. This is the source code for Fiddle::Handle.new

static VALUE
rb_fiddle_handle_initialize(int argc, VALUE argv[], VALUE self)
{
    void *ptr;
    struct dl_handle *fiddle_handle;
    VALUE lib, flag;
    char  *clib;
    int   cflag;
    const char *err;

    switch( rb_scan_args(argc, argv, "02", &lib, &flag) ){
      case 0:
        clib = NULL;
        cflag = RTLD_LAZY | RTLD_GLOBAL;
        break;
      case 1:
        clib = NIL_P(lib) ? NULL : StringValueCStr(lib);
        cflag = RTLD_LAZY | RTLD_GLOBAL;
        break;
      case 2:
        clib = NIL_P(lib) ? NULL : StringValueCStr(lib);
        cflag = NUM2INT(flag);
        break;
      default:
        rb_bug("rb_fiddle_handle_new");
    }

#if defined(_WIN32)
    if( !clib ){
        HANDLE rb_libruby_handle(void);
        ptr = rb_libruby_handle();
    }
    else if( STRCASECMP(clib, "libc") == 0
# ifdef RUBY_COREDLL
             || STRCASECMP(clib, RUBY_COREDLL) == 0
             || STRCASECMP(clib, RUBY_COREDLL".dll") == 0
# endif
        ){
# ifdef _WIN32_WCE
        ptr = dlopen("coredll.dll", cflag);
# else
        (void)cflag;
        ptr = w32_coredll();
# endif
    }
    else
#endif
        ptr = dlopen(clib, cflag);
#if defined(HAVE_DLERROR)
    if( !ptr && (err = dlerror()) ){
        rb_raise(rb_eFiddleDLError, "%s", err);
    }
#else
    if( !ptr ){
        err = dlerror();
        rb_raise(rb_eFiddleDLError, "%s", err);
    }
#endif
    TypedData_Get_Struct(self, struct dl_handle, &fiddle_handle_data_type, fiddle_handle);
    if( fiddle_handle->ptr && fiddle_handle->open && fiddle_handle->enable_close ){
        dlclose(fiddle_handle->ptr);
    }
    fiddle_handle->ptr = ptr;
    fiddle_handle->open = 1;
    fiddle_handle->enable_close = 0;

    if( rb_block_given_p() ){
        rb_ensure(rb_yield, self, rb_fiddle_handle_close, self);
    }

    return Qnil;
}

Yes :slight_smile:

As far as I can test, dlopen seems to search the lib from ENV['Path'] (on windows) if the path doesn’t start with a / .

On the Win10 I have it doesn’t care of the working dir.

What’s the following output:

puts "#{__dir__.encoding.to_s.ljust 12}  #{__dir__}"
%w[external internal locale filesystem].each do |enc|
  puts "#{Encoding.find(enc).to_s.ljust 12}  #{enc}"
end

On Win 10 :

UTF-8         C:/Temp/uby киї_テスト
UTF-8         external
              internal
Windows-1252  locale
Windows-1252  filesystem

There is also script encoding, ie: __ENCODING__
which I always set with a magic comment as the first line of any rb file:

# encoding: UTF-8

My system is Win11, and shows all as UTF-8. You might check the code at used in RubyInstaller2

dll_directory.rb

Or, maybe you could add the folder to the dll search path…

Thank you @MSP_Greg.

dll_directory uses Win32 module. But as the problem also occur on old MacOS, I think the “best” compatible way is to copy the DLL (or dylib) file to Dir.tmpdir folder. Even if this folder is under user name folder on Windows, this path is shorten to the old 8 characters format.

As far as I can test, it works on these old versions of Windows dans Mac. I copy the lib only if firsl dlopen failed.

Interesting. I’ve got older SketchUp releases, lots of older stand-alone Rubies, but I don’t have older OS’s, unless one counts Ubuntu-20.04…

Using the short path may be the best alternative.

Also, see a Ruby issue from 2014, opened by a Sketchup’s own Bugra Barin. Interestingly, it changes LoadLibrary calls to LoadLibraryW. Unfortunately, that was just Ruby core, and Fiddle was left as is.

1 Like