mirror of
https://git.savannah.gnu.org/git/guile.git
synced 2025-04-29 19:30:36 +02:00
The new non-libltdl foreign library loading algorithm from 3.0.6 fails to cover common cases regarding how libtool names and installs DLL files. Notably, it fails to recognize when libtool has added the major version number into the filename itself, such as libfoo-1.dll Also, it does not search in binary directories and the PATH for DLL files, where libtool is likely to install DLLs. This adds the option to search for dlls with major version numbers in the filename, and modifies the search strategy for DLL-using OSs to check bindir and PATH. For MSYS, libraries are installed with the 'msys-' prefix. So this modifies load-foreign-library to handle that prefix as well. It changes the #:rename-on-cygwin? option to #:host-type-rename? to better reflect that is works on both Cygwin and MSYS. Partially based on a patch by Hannes Müller. * NEWS: updated * doc/ref/api-foreign.texi: document updates to load-foreign-library and system-dll-path * module/system/foreign-library.scm (is-integer-string?): new utility function (dll-name-match?): new utility function (find-best-dll-from-matches): new utility function (dll-exists-with-version): new function that implements new dll search logic (file-exists-with-extension): add flag argument to allow new dll search (file-exists-in-path-with-extension): add flag argument to all new dll search (system-dll-path): new parameter (lib->msys): new helper function (load-foreign-library): add new optarg flag #:allow-dll-version-suffix? Pass new flag to library search functions. Implement new search strategy for #:search-system-paths? on DLL systems' replace #:rename-on-cygwin? with #:host-type-rename? Use that option to rename both MSYS and Cygwin libraries. (guile-system-extensions-path): prefer bindir to libdir on DLL systems * test-suite/tests/foreign.test ("dll-name-match?"): new test category ("find-best-dll-from-matches"): new test category ("lib->msys"): new unit tests
374 lines
13 KiB
Scheme
374 lines
13 KiB
Scheme
;;; Dynamically linking foreign libraries via dlopen and dlsym
|
|
;;; Copyright (C) 2021 Free Software Foundation, Inc.
|
|
;;;
|
|
;;; This library is free software: you can redistribute it and/or modify
|
|
;;; it under the terms of the GNU Lesser General Public License as
|
|
;;; published by the Free Software Foundation, either version 3 of the
|
|
;;; License, or (at your option) any later version.
|
|
;;;
|
|
;;; This library is distributed in the hope that it will be useful, but
|
|
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
;;; Lesser General Public License for more details.
|
|
;;;
|
|
;;; You should have received a copy of the GNU Lesser General Public
|
|
;;; License along with this program. If not, see
|
|
;;; <http://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
;;;
|
|
;;; Implementation of dynamic-link.
|
|
;;;
|
|
;;; Code:
|
|
|
|
|
|
(define-module (system foreign-library)
|
|
#:use-module (ice-9 match)
|
|
#:use-module (srfi srfi-9)
|
|
#:use-module (system foreign)
|
|
#:use-module ((ice-9 ftw) #:select (scandir))
|
|
#:use-module ((srfi srfi-1) #:select (find))
|
|
#:export (guile-extensions-path
|
|
ltdl-library-path
|
|
guile-system-extensions-path
|
|
|
|
lib->cyg lib->msys
|
|
load-foreign-library
|
|
foreign-library?
|
|
foreign-library-pointer
|
|
foreign-library-function))
|
|
|
|
(define-record-type <foreign-library>
|
|
(make-foreign-library filename handle)
|
|
foreign-library?
|
|
(filename foreign-library-filename)
|
|
(handle foreign-library-handle set-foreign-library-handle!))
|
|
|
|
(eval-when (expand load eval)
|
|
(load-extension (string-append "libguile-" (effective-version))
|
|
"scm_init_system_foreign_library"))
|
|
|
|
(define system-library-extensions
|
|
(cond
|
|
((string-contains %host-type "-darwin")
|
|
'(".bundle" ".so" ".dylib"))
|
|
((or (string-contains %host-type "cygwin")
|
|
(string-contains %host-type "mingw")
|
|
(string-contains %host-type "msys"))
|
|
'(".dll"))
|
|
(else
|
|
'(".so"))))
|
|
|
|
(define (has-extension? head exts)
|
|
(match exts
|
|
(() #f)
|
|
((ext . exts)
|
|
(or (string-contains head ext)
|
|
(has-extension? head exts)))))
|
|
|
|
(define (is-integer-string? str)
|
|
(and
|
|
(not (string-null? str))
|
|
(let ((n (string-length str)))
|
|
(let lp ((i 0))
|
|
(let ((c (string-ref str i)))
|
|
(if (or (char<? c #\0) (char>? c #\9))
|
|
#f
|
|
(if (= i (1- n))
|
|
#t
|
|
(lp (1+ i)))))))))
|
|
|
|
(define (dll-name-match? src tgt)
|
|
(let ((srclen (string-length src))
|
|
(tgtlen (string-length tgt)))
|
|
(cond
|
|
((or (< srclen tgtlen)
|
|
(< tgtlen 5)
|
|
(string-ci<> src ".dll" (- srclen 4))
|
|
(string-ci<> tgt ".dll" (- tgtlen 4))
|
|
(string-ci<> src tgt 0 (- tgtlen 4) 0 (- tgtlen 4)))
|
|
#f)
|
|
(else
|
|
(let ((mid (substring src (- tgtlen 4) (- srclen 4))))
|
|
(cond
|
|
((or (string-null? mid)
|
|
(and (char=? (string-ref mid 0) #\-)
|
|
(is-integer-string? (string-drop mid 1))))
|
|
#t)
|
|
(else
|
|
#f)))))))
|
|
|
|
(define (find-best-dll-from-matches dllname lst)
|
|
;; A DLL name without a version suffix is preferred,
|
|
;; like libfoo.dll. But if we must have a version
|
|
;; suffix as in libfoo-5.dll, we want the largest one.
|
|
(define (ver>? a b)
|
|
(cond
|
|
((> (string-length a) (string-length b))
|
|
#t)
|
|
((< (string-length a) (string-length b))
|
|
#f)
|
|
(else
|
|
(string-ci>? a b))))
|
|
|
|
(cond
|
|
((null? lst)
|
|
#f)
|
|
((= (length lst) 1)
|
|
(car lst))
|
|
(else
|
|
(or
|
|
(find (lambda (entry) (string-ci= entry dllname)) lst)
|
|
;; The longest string that is alphabetically last
|
|
;; is numerically the highest.
|
|
(car (sort lst ver>?))))))
|
|
|
|
(define (dll-exists-with-version head)
|
|
;; Searches for a DLL given a filepath, allowing
|
|
;; for DLLs with version suffixes.
|
|
(let* ((fname (if (has-extension? head '(".dll"))
|
|
head
|
|
(string-append head ".dll")))
|
|
(dir (dirname fname))
|
|
(base (basename fname)))
|
|
(let ((matches (scandir dir (lambda (f) (dll-name-match? f base)))))
|
|
(if (or (not matches) (null? matches))
|
|
#f
|
|
(in-vicinity dir (find-best-dll-from-matches fname matches))))))
|
|
|
|
(define (file-exists-with-extension head extensions versioned-dlls?)
|
|
(if (has-extension? head extensions)
|
|
(and (file-exists? head) head)
|
|
(let lp ((exts extensions))
|
|
(match exts
|
|
(() (if (and versioned-dlls? (member ".dll" extensions))
|
|
(dll-exists-with-version head)
|
|
#f))
|
|
((ext . exts)
|
|
(let ((head (string-append head ext)))
|
|
(if (file-exists? head)
|
|
head
|
|
(lp exts))))))))
|
|
|
|
(define (file-exists-in-path-with-extension basename path exts versioned-dlls?)
|
|
(match path
|
|
(() #f)
|
|
((dir . path)
|
|
(or (file-exists-with-extension (in-vicinity dir basename) exts versioned-dlls?)
|
|
(file-exists-in-path-with-extension basename path exts versioned-dlls?)))))
|
|
|
|
(define path-separator
|
|
(case (system-file-name-convention)
|
|
((posix) #\:)
|
|
((windows) #\;)
|
|
(else (error "unreachable"))))
|
|
|
|
(define (parse-path var)
|
|
(match (getenv var)
|
|
(#f #f)
|
|
;; Ignore e.g. "export GUILE_SYSTEM_EXTENSIONS_PATH=".
|
|
("" '())
|
|
(val (string-split val path-separator))))
|
|
|
|
(define guile-extensions-path
|
|
(make-parameter
|
|
(or (parse-path "GUILE_EXTENSIONS_PATH") '())))
|
|
|
|
(define ltdl-library-path
|
|
(make-parameter
|
|
(or (parse-path "LTDL_LIBRARY_PATH") '())))
|
|
|
|
(define guile-system-extensions-path
|
|
(make-parameter
|
|
(or (parse-path "GUILE_SYSTEM_EXTENSIONS_PATH")
|
|
(list (if (or (string-contains %host-type "cygwin")
|
|
(string-contains %host-type "mingw")
|
|
(string-contains %host-type "msys"))
|
|
(assq-ref %guile-build-info 'bindir)
|
|
(assq-ref %guile-build-info 'libdir))
|
|
(assq-ref %guile-build-info 'extensiondir)))))
|
|
|
|
(define system-dll-path
|
|
(make-parameter
|
|
(or (parse-path "PATH") '())))
|
|
|
|
;; There are a few messy situations here related to libtool.
|
|
;;
|
|
;; Guile used to use libltdl, the dynamic library loader provided by
|
|
;; libtool. This loader used LTDL_LIBRARY_PATH, and for backwards
|
|
;; compatibility we still support that path.
|
|
;;
|
|
;; However, libltdl would not only open ".so" (or ".dll", etc) files,
|
|
;; but also the ".la" files created by libtool. In installed libraries
|
|
;; -- libraries that are in the target directories of "make install" --
|
|
;; .la files are never needed, to the extent that most GNU/Linux
|
|
;; distributions remove them entirely. It is sufficient to just load
|
|
;; the ".so" (or ".dll", etc) files.
|
|
;;
|
|
;; But for uninstalled dynamic libraries, like those in a build tree, it
|
|
;; is a bit of a mess. If you have a project that uses libtool to build
|
|
;; libraries -- which is the case for Guile, and for most projects using
|
|
;; autotools -- and you build foo.so in directory D, libtool will put
|
|
;; foo.la in D, but foo.so goes in D/.libs.
|
|
;;
|
|
;; The nice thing about ltdl was that it could load the .la file, even
|
|
;; from a build tree, preventing the existence of ".libs" from leaking
|
|
;; out to the user.
|
|
;;
|
|
;; We don't use libltdl now, essentially for flexibility and
|
|
;; error-reporting reasons. But, it would be nice to keep this old
|
|
;; use-case working. So as a stopgap solution, we add a ".libs" subdir
|
|
;; to the path for each entry in LTDL_LIBRARY_PATH, in case the .so is
|
|
;; there instead of alongside the .la file.
|
|
(define (augment-ltdl-library-path path)
|
|
(match path
|
|
(() '())
|
|
((dir . path)
|
|
(cons* dir (in-vicinity dir ".libs")
|
|
(augment-ltdl-library-path path)))))
|
|
|
|
|
|
(define (default-search-path search-ltdl-library-path?)
|
|
(append
|
|
(guile-extensions-path)
|
|
(if search-ltdl-library-path?
|
|
(augment-ltdl-library-path (ltdl-library-path))
|
|
'())
|
|
(guile-system-extensions-path)))
|
|
|
|
(define (lib->cyg name)
|
|
"Convert a standard shared library name to a Cygwin shared library
|
|
name."
|
|
(if (not name)
|
|
#f
|
|
(let ((start (1+ (or (string-index-right
|
|
name
|
|
(lambda (c) (or (char=? #\\ c) (char=? #\/ c))))
|
|
-1))))
|
|
(cond
|
|
((>= (+ 3 start) (string-length name))
|
|
name)
|
|
((string= name "lib" start (+ start 3))
|
|
(string-append (substring name 0 start)
|
|
"cyg"
|
|
(substring name (+ start 3))))
|
|
(else
|
|
name)))))
|
|
|
|
(define (lib->msys name)
|
|
"Convert a standard shared library name to a MSYS shared library
|
|
name."
|
|
(if (not name)
|
|
#f
|
|
(let ((start (1+ (or (string-index-right
|
|
name
|
|
(lambda (c) (or (char=? #\\ c) (char=? #\/ c))))
|
|
-1))))
|
|
(cond
|
|
((>= (+ 3 start) (string-length name))
|
|
name)
|
|
((string= name "lib" start (+ start 3))
|
|
(string-append (substring name 0 start)
|
|
"msys-"
|
|
(substring name (+ start 3))))
|
|
(else
|
|
name)))))
|
|
|
|
(define* (load-foreign-library #:optional filename #:key
|
|
(extensions system-library-extensions)
|
|
(search-ltdl-library-path? #t)
|
|
(search-path (default-search-path
|
|
search-ltdl-library-path?))
|
|
(search-system-paths? #t)
|
|
(lazy? #t) (global? #f) (host-type-rename? #t)
|
|
(allow-dll-version-suffix? #t))
|
|
(define (error-not-found)
|
|
(scm-error 'misc-error "load-foreign-library"
|
|
"file: ~S, message: ~S"
|
|
(list filename "file not found")
|
|
#f))
|
|
(define flags
|
|
(logior (if lazy? RTLD_LAZY RTLD_NOW)
|
|
(if global? RTLD_GLOBAL RTLD_LOCAL)))
|
|
(define (dlopen* name)
|
|
(dlopen name flags))
|
|
(define (file-exists-with-ext filename extensions)
|
|
(file-exists-with-extension filename extensions allow-dll-version-suffix?))
|
|
(define (file-exists-in-path-with-ext filename search-path extensions)
|
|
(file-exists-in-path-with-extension
|
|
filename search-path extensions allow-dll-version-suffix?))
|
|
(when host-type-rename?
|
|
(when (string-contains %host-type "cygwin")
|
|
(set! filename (lib->cyg filename)))
|
|
(when (string-contains %host-type "msys")
|
|
(set! filename (lib->msys filename))))
|
|
(make-foreign-library
|
|
filename
|
|
(cond
|
|
((not filename)
|
|
;; The self-open trick.
|
|
(dlopen* #f))
|
|
((or (absolute-file-name? filename)
|
|
(string-any file-name-separator? filename))
|
|
(cond
|
|
((or (file-exists-with-ext filename extensions)
|
|
(and search-ltdl-library-path?
|
|
(file-exists-with-ext
|
|
(in-vicinity (in-vicinity (dirname filename) ".libs")
|
|
(basename filename))
|
|
extensions)))
|
|
=> dlopen*)
|
|
(else
|
|
(error-not-found))))
|
|
((file-exists-in-path-with-ext filename search-path extensions)
|
|
=> dlopen*)
|
|
((and search-system-paths?
|
|
(or (string-contains %host-type "cygwin")
|
|
(string-contains %host-type "mingw")
|
|
(string-contains %host-type "msys")))
|
|
(let ((fullname (file-exists-in-path-with-ext filename (system-dll-path) '(".dll"))))
|
|
(if fullname
|
|
(dlopen* fullname)
|
|
(error-not-found))))
|
|
|
|
(search-system-paths?
|
|
(if (or (null? extensions) (has-extension? filename extensions))
|
|
(dlopen* filename)
|
|
(let lp ((extensions extensions))
|
|
(match extensions
|
|
((extension)
|
|
;; Open in tail position to propagate any exception.
|
|
(dlopen* (string-append filename extension)))
|
|
((extension . extensions)
|
|
;; If there is more than one extension, unfortunately we
|
|
;; only report the error for the last extension. This is
|
|
;; not great because maybe the library was found with the
|
|
;; first extension, failed to load and had an interesting
|
|
;; error, but then we swallowed that interesting error and
|
|
;; proceeded, eventually throwing a "file not found"
|
|
;; exception. FIXME to use more structured exceptions and
|
|
;; stop if the error that we get is more specific than
|
|
;; just "file not found".
|
|
(or (false-if-exception
|
|
(dlopen* (string-append filename extension)))
|
|
(lp extensions)))))))
|
|
(else
|
|
(error-not-found)))))
|
|
|
|
(define (->foreign-library lib)
|
|
(if (foreign-library? lib)
|
|
lib
|
|
(load-foreign-library lib)))
|
|
|
|
(define* (foreign-library-pointer lib name)
|
|
(let ((handle (foreign-library-handle (->foreign-library lib))))
|
|
(dlsym handle name)))
|
|
|
|
(define* (foreign-library-function lib name
|
|
#:key
|
|
(return-type void)
|
|
(arg-types '())
|
|
(return-errno? #f))
|
|
(let ((pointer (foreign-library-pointer lib name)))
|
|
(pointer->procedure return-type pointer arg-types
|
|
#:return-errno? return-errno?)))
|