;;; 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 ;;; . ;;; 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 (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 #\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?)))