Skip to content

Commit 40ec2a2

Browse files
vlib: add a new dl.loader module, to simplify dynamic library loading, when the DLLs may be in multiple customisable locations (#17161)
1 parent 2d51379 commit 40ec2a2

File tree

7 files changed

+397
-0
lines changed

7 files changed

+397
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module library
2+
3+
// add_1 is exported with the C name `add_1`.
4+
// It can be called by external programs, when the module is compiled
5+
// as a shared library.
6+
// It is exported, because the function is declared as public with `pub`.
7+
// The exported C name is `add_1`, because of the export: tag.
8+
// (Normally, the exported name is a V mangled version based on the module
9+
// name followed by __, followed by the fn name, i.e. it would have been
10+
// `library__add_1`, if not for the export: tag).
11+
[export: 'add_1']
12+
pub fn add_1(x int, y int) int {
13+
return my_private_function(x + y)
14+
}
15+
16+
// this function is not exported and will not be visible to external programs.
17+
fn my_private_function(x int) int {
18+
return 1 + x
19+
}

examples/dynamic_library_loader/use.v

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
module main
2+
3+
// Note: This program, requires that the shared library was already compiled.
4+
// To do so, run `v -d no_backtrace -o library -shared modules/library/library.v`
5+
// before running this program.
6+
import os
7+
import dl
8+
import dl.loader
9+
10+
type FNAdder = fn (int, int) int
11+
12+
const (
13+
cfolder = os.dir(@FILE)
14+
default_paths = [
15+
os.join_path(cfolder, 'library${dl.dl_ext}'),
16+
os.join_path(cfolder, 'location1/library${dl.dl_ext}'),
17+
os.join_path(cfolder, 'location2/library${dl.dl_ext}'),
18+
os.join_path(cfolder, 'modules/library/library${dl.dl_ext}'),
19+
]
20+
)
21+
22+
fn main() {
23+
mut dl_loader := loader.get_or_create_dynamic_lib_loader(
24+
key: cfolder + '/library'
25+
paths: default_paths
26+
)!
27+
defer {
28+
dl_loader.unregister()
29+
}
30+
sym := dl_loader.get_sym('add_1')!
31+
f := FNAdder(sym)
32+
eprintln('f: ${ptr_str(f)}')
33+
res := f(1, 2)
34+
eprintln('res: ${res}')
35+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
module main
2+
3+
import os
4+
import dl
5+
6+
const (
7+
vexe = os.real_path(os.getenv('VEXE'))
8+
so_ext = dl.dl_ext
9+
)
10+
11+
fn test_vexe() {
12+
// dump(vexe)
13+
assert vexe != ''
14+
// dump(os.executable())
15+
// dump(@FILE)
16+
// dump(cfolder)
17+
// dump(so_ext)
18+
}
19+
20+
fn test_can_compile_library() {
21+
os.chdir(cfolder) or {}
22+
library_file_path := os.join_path(cfolder, dl.get_libname('library'))
23+
os.rm(library_file_path) or {}
24+
v_compile('-d no_backtrace -o library -shared modules/library/library.v')
25+
assert os.is_file(library_file_path)
26+
}
27+
28+
fn test_can_compile_main_program() {
29+
os.chdir(cfolder) or {}
30+
library_file_path := os.join_path(cfolder, dl.get_libname('library'))
31+
assert os.is_file(library_file_path)
32+
result := v_compile('run use.v')
33+
// dump(result)
34+
assert result.output.contains('res: 4')
35+
os.rm(library_file_path) or {}
36+
}
37+
38+
fn test_can_compile_and_use_library_with_skip_unused_home_dir() {
39+
os.chdir(cfolder) or {}
40+
library_file_path := os.join_path(cfolder, dl.get_libname('library'))
41+
os.rm(library_file_path) or {}
42+
v_compile('-skip-unused -d no_backtrace -o library -shared modules/library/library.v')
43+
assert os.is_file(library_file_path)
44+
result := v_compile('run use.v')
45+
assert result.output.contains('res: 4')
46+
os.rm(library_file_path) or {}
47+
}
48+
49+
fn test_can_compile_and_use_library_with_skip_unused_location1_dir() {
50+
os.chdir(cfolder) or {}
51+
library_file_path := os.join_path(cfolder, 'location1', dl.get_libname('library'))
52+
os.rm(library_file_path) or {}
53+
os.mkdir('location1') or {}
54+
v_compile('-skip-unused -d no_backtrace -o location1/library -shared modules/library/library.v')
55+
assert os.is_file(library_file_path)
56+
result := v_compile('run use.v')
57+
assert result.output.contains('res: 4')
58+
os.rm(library_file_path) or {}
59+
}
60+
61+
fn v_compile(vopts string) os.Result {
62+
cmd := '${os.quoted_path(vexe)} -showcc ${vopts}'
63+
// dump(cmd)
64+
res := os.execute_or_exit(cmd)
65+
// dump(res)
66+
assert res.exit_code == 0
67+
return res
68+
}

vlib/dl/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
It is a thin wrapper over `LoadLibrary` on Windows, and `dlopen` on Unix.
55

66
Using it, you can implement a plugin system for your application.
7+
8+
> NOTE: We highly recommend using `dl.loader` instead of `dl` directly.
9+
> It provides a more user-friendly API in the V way.

vlib/dl/loader/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
## Description:
2+
3+
`dl.loader` is an abstraction layer over `dl` that provides a more user-friendly API in the V way.
4+
It can be used to Dynamically Load a library during runtime in scenarios where the library to load
5+
does not have a determined path an can be located in different places.
6+
7+
It also provides a way to load a library from a specific path, or from a list of paths, or from
8+
a custom environment variable that contains a list of paths.
9+
10+
## Usage:
11+
12+
```v
13+
import dl.loader
14+
15+
// Load a library from a list of paths
16+
const default_paths = [
17+
'not-existing-dynamic-link-library'
18+
// 'C:\\Windows\\System32\\shell32.dll',
19+
'shell32',
20+
]
21+
22+
fn main() {
23+
mut dl_loader := loader.get_or_create_dynamic_lib_loader(
24+
key: 'LibExample'
25+
env_path: 'LIB_PATH'
26+
paths: default_paths
27+
)!
28+
29+
defer {
30+
dl_loader.unregister()
31+
}
32+
33+
sym := dl_loader.get_sym('CommandLineToArgvW')!
34+
assert !isnil(sym)
35+
}
36+
```

vlib/dl/loader/loader.v

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
[has_globals]
2+
module loader
3+
4+
import dl
5+
import os
6+
7+
const (
8+
dl_no_path_issue_msg = 'no paths to dynamic library'
9+
dl_open_issue_msg = 'could not open dynamic library'
10+
dl_sym_issue_msg = 'could not get optional symbol from dynamic library'
11+
dl_close_issue_msg = 'could not close dynamic library'
12+
dl_register_issue_msg = 'could not register dynamic library loader'
13+
)
14+
15+
pub const (
16+
dl_no_path_issue_code = 1
17+
dl_open_issue_code = 1
18+
dl_sym_issue_code = 2
19+
dl_close_issue_code = 3
20+
dl_register_issue_code = 4
21+
22+
dl_no_path_issue_err = error_with_code(dl_no_path_issue_msg, dl_no_path_issue_code)
23+
dl_open_issue_err = error_with_code(dl_open_issue_msg, dl_open_issue_code)
24+
dl_sym_issue_err = error_with_code(dl_sym_issue_msg, dl_sym_issue_code)
25+
dl_close_issue_err = error_with_code(dl_close_issue_msg, dl_close_issue_code)
26+
dl_register_issue_err = error_with_code(dl_register_issue_msg, dl_register_issue_code)
27+
)
28+
29+
__global (
30+
registered_dl_loaders map[string]&DynamicLibLoader
31+
)
32+
33+
fn register_dl_loader(dl_loader &DynamicLibLoader) ! {
34+
if dl_loader.key in registered_dl_loaders {
35+
return loader.dl_register_issue_err
36+
}
37+
registered_dl_loaders[dl_loader.key] = dl_loader
38+
}
39+
40+
// registered_dl_loader_keys returns the keys of registered DynamicLibLoader.
41+
pub fn registered_dl_loader_keys() []string {
42+
return registered_dl_loaders.keys()
43+
}
44+
45+
// DynamicLibLoader is a wrapper around dlopen, dlsym and dlclose.
46+
[heap]
47+
pub struct DynamicLibLoader {
48+
pub:
49+
key string
50+
flags int = dl.rtld_lazy
51+
paths []string
52+
mut:
53+
handle voidptr
54+
sym_map map[string]voidptr
55+
}
56+
57+
// DynamicLibLoaderConfig is a configuration for DynamicLibLoader.
58+
[params]
59+
pub struct DynamicLibLoaderConfig {
60+
// flags is the flags for dlopen.
61+
flags int = dl.rtld_lazy
62+
// key is the key to register the DynamicLibLoader.
63+
key string
64+
// env_path is the environment variable name that contains the path to the dynamic library.
65+
env_path string
66+
// paths is the list of paths to the dynamic library.
67+
paths []string
68+
}
69+
70+
// new_dynamic_lib_loader returns a new DynamicLibLoader.
71+
fn new_dynamic_lib_loader(conf DynamicLibLoaderConfig) !&DynamicLibLoader {
72+
mut paths := []string{}
73+
74+
if conf.env_path.len > 0 {
75+
if env_path := os.getenv_opt(conf.env_path) {
76+
paths << env_path.split(os.path_delimiter)
77+
}
78+
}
79+
80+
paths << conf.paths
81+
82+
if paths.len == 0 {
83+
return loader.dl_no_path_issue_err
84+
}
85+
86+
mut dl_loader := &DynamicLibLoader{
87+
key: conf.key
88+
flags: conf.flags
89+
paths: paths
90+
}
91+
92+
register_dl_loader(dl_loader)!
93+
return dl_loader
94+
}
95+
96+
// get_or_create_dynamic_lib_loader returns a DynamicLibLoader.
97+
// If the DynamicLibLoader is not registered, it creates a new DynamicLibLoader.
98+
pub fn get_or_create_dynamic_lib_loader(conf DynamicLibLoaderConfig) !&DynamicLibLoader {
99+
if dl_loader := registered_dl_loaders[conf.key] {
100+
return dl_loader
101+
}
102+
return new_dynamic_lib_loader(conf)
103+
}
104+
105+
// load loads the dynamic library.
106+
pub fn (mut dl_loader DynamicLibLoader) open() !voidptr {
107+
if !isnil(dl_loader.handle) {
108+
return dl_loader.handle
109+
}
110+
111+
for path in dl_loader.paths {
112+
if handle := dl.open_opt(path, dl_loader.flags) {
113+
dl_loader.handle = handle
114+
return handle
115+
}
116+
}
117+
118+
return loader.dl_open_issue_err
119+
}
120+
121+
// close closes the dynamic library.
122+
pub fn (mut dl_loader DynamicLibLoader) close() ! {
123+
if !isnil(dl_loader.handle) {
124+
if dl.close(dl_loader.handle) {
125+
dl_loader.handle = unsafe { nil }
126+
return
127+
}
128+
}
129+
130+
return loader.dl_close_issue_err
131+
}
132+
133+
// get_sym gets a symbol from the dynamic library.
134+
pub fn (mut dl_loader DynamicLibLoader) get_sym(name string) !voidptr {
135+
if sym := dl_loader.sym_map[name] {
136+
return sym
137+
}
138+
139+
handle := dl_loader.open()!
140+
if sym := dl.sym_opt(handle, name) {
141+
dl_loader.sym_map[name] = sym
142+
return sym
143+
}
144+
145+
dl_loader.close()!
146+
return loader.dl_sym_issue_err
147+
}
148+
149+
// unregister unregisters the DynamicLibLoader.
150+
pub fn (mut dl_loader DynamicLibLoader) unregister() {
151+
dl_loader.close() or {}
152+
registered_dl_loaders.delete(dl_loader.key)
153+
}

0 commit comments

Comments
 (0)