From 40ec2a292e58bcb5f111507c8fe5da59d1804def Mon Sep 17 00:00:00 2001 From: Ulises Jeremias Cornejo Fandos Date: Tue, 31 Jan 2023 04:27:48 -0300 Subject: [PATCH] vlib: add a new dl.loader module, to simplify dynamic library loading, when the DLLs may be in multiple customisable locations (#17161) --- .../modules/library/library.v | 19 +++ examples/dynamic_library_loader/use.v | 35 ++++ examples/dynamic_library_loader/use_test.v | 68 ++++++++ vlib/dl/README.md | 3 + vlib/dl/loader/README.md | 36 +++++ vlib/dl/loader/loader.v | 153 ++++++++++++++++++ vlib/dl/loader/loader_test.v | 83 ++++++++++ 7 files changed, 397 insertions(+) create mode 100644 examples/dynamic_library_loader/modules/library/library.v create mode 100644 examples/dynamic_library_loader/use.v create mode 100644 examples/dynamic_library_loader/use_test.v create mode 100644 vlib/dl/loader/README.md create mode 100644 vlib/dl/loader/loader.v create mode 100644 vlib/dl/loader/loader_test.v diff --git a/examples/dynamic_library_loader/modules/library/library.v b/examples/dynamic_library_loader/modules/library/library.v new file mode 100644 index 000000000..ed3658c91 --- /dev/null +++ b/examples/dynamic_library_loader/modules/library/library.v @@ -0,0 +1,19 @@ +module library + +// add_1 is exported with the C name `add_1`. +// It can be called by external programs, when the module is compiled +// as a shared library. +// It is exported, because the function is declared as public with `pub`. +// The exported C name is `add_1`, because of the export: tag. +// (Normally, the exported name is a V mangled version based on the module +// name followed by __, followed by the fn name, i.e. it would have been +// `library__add_1`, if not for the export: tag). +[export: 'add_1'] +pub fn add_1(x int, y int) int { + return my_private_function(x + y) +} + +// this function is not exported and will not be visible to external programs. +fn my_private_function(x int) int { + return 1 + x +} diff --git a/examples/dynamic_library_loader/use.v b/examples/dynamic_library_loader/use.v new file mode 100644 index 000000000..97e4334f1 --- /dev/null +++ b/examples/dynamic_library_loader/use.v @@ -0,0 +1,35 @@ +module main + +// Note: This program, requires that the shared library was already compiled. +// To do so, run `v -d no_backtrace -o library -shared modules/library/library.v` +// before running this program. +import os +import dl +import dl.loader + +type FNAdder = fn (int, int) int + +const ( + cfolder = os.dir(@FILE) + default_paths = [ + os.join_path(cfolder, 'library${dl.dl_ext}'), + os.join_path(cfolder, 'location1/library${dl.dl_ext}'), + os.join_path(cfolder, 'location2/library${dl.dl_ext}'), + os.join_path(cfolder, 'modules/library/library${dl.dl_ext}'), + ] +) + +fn main() { + mut dl_loader := loader.get_or_create_dynamic_lib_loader( + key: cfolder + '/library' + paths: default_paths + )! + defer { + dl_loader.unregister() + } + sym := dl_loader.get_sym('add_1')! + f := FNAdder(sym) + eprintln('f: ${ptr_str(f)}') + res := f(1, 2) + eprintln('res: ${res}') +} diff --git a/examples/dynamic_library_loader/use_test.v b/examples/dynamic_library_loader/use_test.v new file mode 100644 index 000000000..a939e990f --- /dev/null +++ b/examples/dynamic_library_loader/use_test.v @@ -0,0 +1,68 @@ +module main + +import os +import dl + +const ( + vexe = os.real_path(os.getenv('VEXE')) + so_ext = dl.dl_ext +) + +fn test_vexe() { + // dump(vexe) + assert vexe != '' + // dump(os.executable()) + // dump(@FILE) + // dump(cfolder) + // dump(so_ext) +} + +fn test_can_compile_library() { + os.chdir(cfolder) or {} + library_file_path := os.join_path(cfolder, dl.get_libname('library')) + os.rm(library_file_path) or {} + v_compile('-d no_backtrace -o library -shared modules/library/library.v') + assert os.is_file(library_file_path) +} + +fn test_can_compile_main_program() { + os.chdir(cfolder) or {} + library_file_path := os.join_path(cfolder, dl.get_libname('library')) + assert os.is_file(library_file_path) + result := v_compile('run use.v') + // dump(result) + assert result.output.contains('res: 4') + os.rm(library_file_path) or {} +} + +fn test_can_compile_and_use_library_with_skip_unused_home_dir() { + os.chdir(cfolder) or {} + library_file_path := os.join_path(cfolder, dl.get_libname('library')) + os.rm(library_file_path) or {} + v_compile('-skip-unused -d no_backtrace -o library -shared modules/library/library.v') + assert os.is_file(library_file_path) + result := v_compile('run use.v') + assert result.output.contains('res: 4') + os.rm(library_file_path) or {} +} + +fn test_can_compile_and_use_library_with_skip_unused_location1_dir() { + os.chdir(cfolder) or {} + library_file_path := os.join_path(cfolder, 'location1', dl.get_libname('library')) + os.rm(library_file_path) or {} + os.mkdir('location1') or {} + v_compile('-skip-unused -d no_backtrace -o location1/library -shared modules/library/library.v') + assert os.is_file(library_file_path) + result := v_compile('run use.v') + assert result.output.contains('res: 4') + os.rm(library_file_path) or {} +} + +fn v_compile(vopts string) os.Result { + cmd := '${os.quoted_path(vexe)} -showcc ${vopts}' + // dump(cmd) + res := os.execute_or_exit(cmd) + // dump(res) + assert res.exit_code == 0 + return res +} diff --git a/vlib/dl/README.md b/vlib/dl/README.md index e6e5d2d46..4cc82cdbb 100644 --- a/vlib/dl/README.md +++ b/vlib/dl/README.md @@ -4,3 +4,6 @@ It is a thin wrapper over `LoadLibrary` on Windows, and `dlopen` on Unix. Using it, you can implement a plugin system for your application. + +> NOTE: We highly recommend using `dl.loader` instead of `dl` directly. +> It provides a more user-friendly API in the V way. diff --git a/vlib/dl/loader/README.md b/vlib/dl/loader/README.md new file mode 100644 index 000000000..5a9580ba8 --- /dev/null +++ b/vlib/dl/loader/README.md @@ -0,0 +1,36 @@ +## Description: + +`dl.loader` is an abstraction layer over `dl` that provides a more user-friendly API in the V way. +It can be used to Dynamically Load a library during runtime in scenarios where the library to load +does not have a determined path an can be located in different places. + +It also provides a way to load a library from a specific path, or from a list of paths, or from +a custom environment variable that contains a list of paths. + +## Usage: + +```v +import dl.loader + +// Load a library from a list of paths +const default_paths = [ + 'not-existing-dynamic-link-library' + // 'C:\\Windows\\System32\\shell32.dll', + 'shell32', +] + +fn main() { + mut dl_loader := loader.get_or_create_dynamic_lib_loader( + key: 'LibExample' + env_path: 'LIB_PATH' + paths: default_paths + )! + + defer { + dl_loader.unregister() + } + + sym := dl_loader.get_sym('CommandLineToArgvW')! + assert !isnil(sym) +} +``` diff --git a/vlib/dl/loader/loader.v b/vlib/dl/loader/loader.v new file mode 100644 index 000000000..b22c65b66 --- /dev/null +++ b/vlib/dl/loader/loader.v @@ -0,0 +1,153 @@ +[has_globals] +module loader + +import dl +import os + +const ( + dl_no_path_issue_msg = 'no paths to dynamic library' + dl_open_issue_msg = 'could not open dynamic library' + dl_sym_issue_msg = 'could not get optional symbol from dynamic library' + dl_close_issue_msg = 'could not close dynamic library' + dl_register_issue_msg = 'could not register dynamic library loader' +) + +pub const ( + dl_no_path_issue_code = 1 + dl_open_issue_code = 1 + dl_sym_issue_code = 2 + dl_close_issue_code = 3 + dl_register_issue_code = 4 + + dl_no_path_issue_err = error_with_code(dl_no_path_issue_msg, dl_no_path_issue_code) + dl_open_issue_err = error_with_code(dl_open_issue_msg, dl_open_issue_code) + dl_sym_issue_err = error_with_code(dl_sym_issue_msg, dl_sym_issue_code) + dl_close_issue_err = error_with_code(dl_close_issue_msg, dl_close_issue_code) + dl_register_issue_err = error_with_code(dl_register_issue_msg, dl_register_issue_code) +) + +__global ( + registered_dl_loaders map[string]&DynamicLibLoader +) + +fn register_dl_loader(dl_loader &DynamicLibLoader) ! { + if dl_loader.key in registered_dl_loaders { + return loader.dl_register_issue_err + } + registered_dl_loaders[dl_loader.key] = dl_loader +} + +// registered_dl_loader_keys returns the keys of registered DynamicLibLoader. +pub fn registered_dl_loader_keys() []string { + return registered_dl_loaders.keys() +} + +// DynamicLibLoader is a wrapper around dlopen, dlsym and dlclose. +[heap] +pub struct DynamicLibLoader { +pub: + key string + flags int = dl.rtld_lazy + paths []string +mut: + handle voidptr + sym_map map[string]voidptr +} + +// DynamicLibLoaderConfig is a configuration for DynamicLibLoader. +[params] +pub struct DynamicLibLoaderConfig { + // flags is the flags for dlopen. + flags int = dl.rtld_lazy + // key is the key to register the DynamicLibLoader. + key string + // env_path is the environment variable name that contains the path to the dynamic library. + env_path string + // paths is the list of paths to the dynamic library. + paths []string +} + +// new_dynamic_lib_loader returns a new DynamicLibLoader. +fn new_dynamic_lib_loader(conf DynamicLibLoaderConfig) !&DynamicLibLoader { + mut paths := []string{} + + if conf.env_path.len > 0 { + if env_path := os.getenv_opt(conf.env_path) { + paths << env_path.split(os.path_delimiter) + } + } + + paths << conf.paths + + if paths.len == 0 { + return loader.dl_no_path_issue_err + } + + mut dl_loader := &DynamicLibLoader{ + key: conf.key + flags: conf.flags + paths: paths + } + + register_dl_loader(dl_loader)! + return dl_loader +} + +// get_or_create_dynamic_lib_loader returns a DynamicLibLoader. +// If the DynamicLibLoader is not registered, it creates a new DynamicLibLoader. +pub fn get_or_create_dynamic_lib_loader(conf DynamicLibLoaderConfig) !&DynamicLibLoader { + if dl_loader := registered_dl_loaders[conf.key] { + return dl_loader + } + return new_dynamic_lib_loader(conf) +} + +// load loads the dynamic library. +pub fn (mut dl_loader DynamicLibLoader) open() !voidptr { + if !isnil(dl_loader.handle) { + return dl_loader.handle + } + + for path in dl_loader.paths { + if handle := dl.open_opt(path, dl_loader.flags) { + dl_loader.handle = handle + return handle + } + } + + return loader.dl_open_issue_err +} + +// close closes the dynamic library. +pub fn (mut dl_loader DynamicLibLoader) close() ! { + if !isnil(dl_loader.handle) { + if dl.close(dl_loader.handle) { + dl_loader.handle = unsafe { nil } + return + } + } + + return loader.dl_close_issue_err +} + +// get_sym gets a symbol from the dynamic library. +pub fn (mut dl_loader DynamicLibLoader) get_sym(name string) !voidptr { + if sym := dl_loader.sym_map[name] { + return sym + } + + handle := dl_loader.open()! + if sym := dl.sym_opt(handle, name) { + dl_loader.sym_map[name] = sym + return sym + } + + dl_loader.close()! + return loader.dl_sym_issue_err +} + +// unregister unregisters the DynamicLibLoader. +pub fn (mut dl_loader DynamicLibLoader) unregister() { + dl_loader.close() or {} + registered_dl_loaders.delete(dl_loader.key) +} diff --git a/vlib/dl/loader/loader_test.v b/vlib/dl/loader/loader_test.v new file mode 100644 index 000000000..67f8146dc --- /dev/null +++ b/vlib/dl/loader/loader_test.v @@ -0,0 +1,83 @@ +import dl.loader +import dl + +fn test_dl_loader() ! { + $if linux { + run_test_invalid_lib_linux()! + return + } + $if windows { + run_test_invalid_lib_windows()! + run_test_valid_lib_windows()! + run_test_invalid_sym_windows()! + run_test_valid_sym_windows()! + return + } $else { + eprint('currently not implemented on this platform') + } +} + +fn get_or_create_loader(name string, paths []string) !&loader.DynamicLibLoader { + return loader.get_or_create_dynamic_lib_loader( + key: name + paths: paths + flags: dl.rtld_now + ) +} + +fn run_test_invalid_lib_linux() ! { + // ensure a not-existing dl won't be loaded + mut dl_loader := get_or_create_loader(@MOD + '.' + @FN + '.' + 'lib', [ + 'not-existing-dynamic-link-library', + ])! + defer { + dl_loader.unregister() + } + h := dl_loader.open() or { unsafe { nil } } + assert isnil(h) +} + +fn run_test_invalid_lib_windows() ! { + // ensure a not-existing dl won't be loaded + mut dl_loader := get_or_create_loader(@MOD + '.' + @FN + '.' + 'lib', [ + 'not-existing-dynamic-link-library', + ])! + defer { + dl_loader.unregister() + } + h := dl_loader.open() or { unsafe { nil } } + assert isnil(h) +} + +fn run_test_valid_lib_windows() ! { + mut dl_loader := get_or_create_loader(@MOD + '.' + @FN + '.' + 'lib', [ + 'not-existing-dynamic-link-library', + 'shell32', + ])! + defer { + dl_loader.unregister() + } + h := dl_loader.open() or { unsafe { nil } } + assert !isnil(h) +} + +fn run_test_invalid_sym_windows() ! { + mut dl_loader := get_or_create_loader(@MOD + '.' + @FN + '.' + 'lib', ['shell32'])! + defer { + dl_loader.unregister() + } + proc := dl_loader.get_sym('CommandLineToArgvW2') or { unsafe { nil } } + assert isnil(proc) +} + +fn run_test_valid_sym_windows() ! { + mut dl_loader := get_or_create_loader(@MOD + '.' + @FN + '.' + 'lib', [ + 'not-existing-dynamic-link-library', + 'shell32', + ])! + defer { + dl_loader.unregister() + } + proc := dl_loader.get_sym('CommandLineToArgvW') or { unsafe { nil } } + assert !isnil(proc) +} -- 2.30.2