libcall 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +69 -13
- data/lib/libcall/caller.rb +62 -8
- data/lib/libcall/cli.rb +174 -79
- data/lib/libcall/library_finder.rb +48 -23
- data/lib/libcall/parser.rb +45 -34
- data/lib/libcall/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 39da3018356d7d6e8287d59bb0ca8ccc24f5a8b211d974b51d526d21347ac0f5
|
|
4
|
+
data.tar.gz: 6d55af828e7e621226bab1efff15174f00e1c542af390cf8e5886f1bbd8369c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bda1f14c0ae9d52f1b8edff83a8c949df3742f12e280ba87c562c1dc958507d329b934c3a87a96d3b14975a2751852bc3498e3c4e99fab3963e32888e81c4e57
|
|
7
|
+
data.tar.gz: 3621dad8e6ad6d5e3ea3ba52970d87457f4fb3dcb03824f75bd88148424a150ff6e6d4565fe1780c859ffc3787750f78905c8afa4fb24e87341b8a90ab3edd74
|
data/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# libcall
|
|
2
2
|
|
|
3
3
|
[](https://github.com/kojix2/libcall/actions/workflows/main.yml)
|
|
4
|
+
[](https://badge.fury.io/rb/libcall)
|
|
4
5
|
[](https://tokei.kojix2.net/github/kojix2/libcall)
|
|
5
6
|
|
|
6
7
|
Call C functions in shared libraries from the command line.
|
|
@@ -11,54 +12,91 @@ Call C functions in shared libraries from the command line.
|
|
|
11
12
|
gem install libcall
|
|
12
13
|
```
|
|
13
14
|
|
|
15
|
+
**Windows**: Supports DLLs (e.g., `msvcrt.dll`, `kernel32.dll`). Searches in System32, PATH, and MSYS2/MinGW directories. For building custom DLLs, RubyInstaller with DevKit is recommended.
|
|
16
|
+
|
|
14
17
|
## Usage
|
|
15
18
|
|
|
16
19
|
```sh
|
|
17
|
-
libcall [OPTIONS] <LIBRARY> <FUNCTION>
|
|
20
|
+
libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
|
|
18
21
|
```
|
|
19
22
|
|
|
20
23
|
### Quick Examples
|
|
21
24
|
|
|
22
25
|
```sh
|
|
23
|
-
#
|
|
24
|
-
libcall -lm
|
|
26
|
+
# TYPE VALUE pairs
|
|
27
|
+
libcall -lm -r f64 sqrt double 16
|
|
25
28
|
# => 4.0
|
|
26
29
|
|
|
27
30
|
# Custom library
|
|
28
|
-
libcall ./mylib.so
|
|
31
|
+
libcall ./mylib.so add_i32 int 10 int 20 -r i32
|
|
29
32
|
# => 30
|
|
30
33
|
```
|
|
31
34
|
|
|
32
|
-
###
|
|
35
|
+
### Argument Syntax
|
|
36
|
+
|
|
37
|
+
Pass arguments as TYPE VALUE pairs (single-token suffix style has been removed):
|
|
38
|
+
|
|
39
|
+
- Examples: `int 10`, `double -3.14`, `string "hello"`
|
|
40
|
+
- Negative values are safe (not treated as options): `int -23`
|
|
41
|
+
|
|
42
|
+
Pointers and null:
|
|
33
43
|
|
|
34
|
-
Use
|
|
44
|
+
- Use `ptr` (or `pointer`) to pass raw addresses as integers
|
|
45
|
+
- Use `null`, `nil`, `NULL`, or `0` to pass a null pointer
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
# Pass a null pointer to a function taking const char*
|
|
49
|
+
libcall -ltest str_length ptr null -r i32
|
|
50
|
+
# => 0
|
|
51
|
+
```
|
|
35
52
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
53
|
+
End of options `--`:
|
|
54
|
+
|
|
55
|
+
- Use `--` to stop option parsing if a value starts with `-`
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
libcall -lc getenv string -- -r -r cstr
|
|
59
|
+
```
|
|
39
60
|
|
|
40
61
|
### Options
|
|
41
62
|
|
|
42
63
|
- `-l LIBRARY` - library name (searches standard paths)
|
|
43
64
|
- `-L PATH` - add library search path
|
|
44
65
|
- `-r TYPE` - return type (void, i32, f64, cstr, ptr)
|
|
66
|
+
- Options may appear before or after the function name.
|
|
45
67
|
- `--dry-run` - validate without executing
|
|
46
68
|
- `--json` - JSON output
|
|
47
69
|
- `--verbose` - detailed info
|
|
48
70
|
- `-h, --help` - show help
|
|
49
71
|
- `-v, --version` - show version
|
|
50
72
|
|
|
73
|
+
Library search:
|
|
74
|
+
|
|
75
|
+
- `-L` adds search paths; `-l` resolves by name
|
|
76
|
+
- On Linux and macOS, `LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` are honored
|
|
77
|
+
|
|
51
78
|
### More Examples
|
|
52
79
|
|
|
53
80
|
```sh
|
|
54
81
|
# JSON output
|
|
55
|
-
libcall --json -lm sqrt 9.
|
|
82
|
+
libcall --json -lm sqrt double 9.0 -r f64
|
|
56
83
|
|
|
57
84
|
# Dry run
|
|
58
|
-
libcall --dry-run ./mylib.so test
|
|
85
|
+
libcall --dry-run ./mylib.so test u64 42 -r void
|
|
59
86
|
|
|
60
87
|
# Using -L and -l (like gcc)
|
|
61
|
-
libcall -lmylib -L./build
|
|
88
|
+
libcall -lmylib -L./build add_i32 int 10 int 20 -r i32
|
|
89
|
+
|
|
90
|
+
# TYPE/VALUE pairs with -r after function
|
|
91
|
+
libcall -lm fabs double -5.5 -r f64
|
|
92
|
+
# => 5.5
|
|
93
|
+
|
|
94
|
+
# Windows: calling C runtime functions
|
|
95
|
+
libcall msvcrt.dll sqrt double 16.0 -r f64
|
|
96
|
+
# => 4.0
|
|
97
|
+
|
|
98
|
+
# Windows: accessing environment variables
|
|
99
|
+
libcall msvcrt.dll getenv string "PATH" -r cstr
|
|
62
100
|
```
|
|
63
101
|
|
|
64
102
|
## Type Reference
|
|
@@ -76,12 +114,30 @@ libcall -lmylib -L./build add 10i32 20i32 -r i32
|
|
|
76
114
|
| `f32` | 32-bit float | single precision |
|
|
77
115
|
| `f64` | 64-bit float | double precision |
|
|
78
116
|
|
|
117
|
+
Also supported:
|
|
118
|
+
|
|
119
|
+
- `string`: C string argument (char\*)
|
|
120
|
+
- `cstr`: C string return (char\*)
|
|
121
|
+
- `ptr`/`pointer`: void\* pointer
|
|
122
|
+
|
|
79
123
|
## pkg-config Support
|
|
80
124
|
|
|
81
125
|
Set `PKG_CONFIG_PATH` and use package names with `-l`:
|
|
82
126
|
|
|
83
127
|
```sh
|
|
84
|
-
PKG_CONFIG_PATH=/path/to/pkgconfig libcall -lmypackage func
|
|
128
|
+
PKG_CONFIG_PATH=/path/to/pkgconfig libcall -lmypackage func i32 42 -r i32
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Output parameters (out:TYPE)
|
|
132
|
+
|
|
133
|
+
You can pass output pointers by specifying `out:TYPE`. The pointer is allocated automatically, passed to the function, and printed after the call.
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
# void get_version(int* major, int* minor)
|
|
137
|
+
libcall -ltest -L ./test/fixtures/libtest/build get_version out:int out:int -r void
|
|
138
|
+
|
|
139
|
+
# JSON includes an "outputs" array
|
|
140
|
+
libcall --json -ltest -L ./test/fixtures/libtest/build get_version out:int out:int -r void
|
|
85
141
|
```
|
|
86
142
|
|
|
87
143
|
## Warning
|
data/lib/libcall/caller.rb
CHANGED
|
@@ -3,24 +3,33 @@
|
|
|
3
3
|
require 'fiddle'
|
|
4
4
|
|
|
5
5
|
module Libcall
|
|
6
|
+
# Execute C function calls via Fiddle FFI
|
|
6
7
|
class Caller
|
|
7
|
-
attr_reader :lib_path, :func_name, :return_type, :
|
|
8
|
+
attr_reader :lib_path, :func_name, :return_type, :arg_pairs
|
|
8
9
|
|
|
9
|
-
def initialize(lib_path, func_name,
|
|
10
|
+
def initialize(lib_path, func_name, arg_pairs: [], return_type: :void)
|
|
10
11
|
@lib_path = lib_path
|
|
11
12
|
@func_name = func_name
|
|
12
13
|
@return_type = return_type
|
|
13
|
-
@
|
|
14
|
+
@arg_pairs = arg_pairs
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def call
|
|
17
18
|
arg_types = []
|
|
18
19
|
arg_values = []
|
|
20
|
+
out_refs = []
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
type_sym, value = Parser.parse_arg(arg)
|
|
22
|
+
arg_pairs.each_with_index do |(type_sym, value), idx|
|
|
22
23
|
arg_types << Parser.fiddle_type(type_sym)
|
|
23
|
-
|
|
24
|
+
|
|
25
|
+
if type_sym.is_a?(Array) && type_sym.first == :out
|
|
26
|
+
inner = type_sym[1]
|
|
27
|
+
ptr = allocate_output_pointer(inner)
|
|
28
|
+
out_refs << { index: idx, type: inner, ptr: ptr }
|
|
29
|
+
arg_values << ptr.to_i
|
|
30
|
+
else
|
|
31
|
+
arg_values << value
|
|
32
|
+
end
|
|
24
33
|
end
|
|
25
34
|
|
|
26
35
|
ret_type = Parser.fiddle_type(return_type)
|
|
@@ -29,9 +38,14 @@ module Libcall
|
|
|
29
38
|
func_ptr = handle[func_name]
|
|
30
39
|
func = Fiddle::Function.new(func_ptr, arg_types, ret_type)
|
|
31
40
|
|
|
32
|
-
|
|
41
|
+
raw_result = func.call(*arg_values)
|
|
42
|
+
formatted_result = format_result(raw_result, return_type)
|
|
33
43
|
|
|
34
|
-
|
|
44
|
+
if out_refs.empty?
|
|
45
|
+
formatted_result
|
|
46
|
+
else
|
|
47
|
+
{ result: formatted_result, outputs: read_output_values(out_refs) }
|
|
48
|
+
end
|
|
35
49
|
rescue Fiddle::DLError => e
|
|
36
50
|
raise Error, "Failed to load library or function: #{e.message}"
|
|
37
51
|
end
|
|
@@ -59,5 +73,45 @@ module Libcall
|
|
|
59
73
|
result
|
|
60
74
|
end
|
|
61
75
|
end
|
|
76
|
+
|
|
77
|
+
def allocate_output_pointer(type_sym)
|
|
78
|
+
size = case type_sym
|
|
79
|
+
when :char, :uchar then Fiddle::SIZEOF_CHAR
|
|
80
|
+
when :short, :ushort then Fiddle::SIZEOF_SHORT
|
|
81
|
+
when :int, :uint then Fiddle::SIZEOF_INT
|
|
82
|
+
when :long, :ulong then Fiddle::SIZEOF_LONG
|
|
83
|
+
when :long_long, :ulong_long then Fiddle::SIZEOF_LONG_LONG
|
|
84
|
+
when :float then Fiddle::SIZEOF_FLOAT
|
|
85
|
+
when :double then Fiddle::SIZEOF_DOUBLE
|
|
86
|
+
when :voidp then Fiddle::SIZEOF_VOIDP
|
|
87
|
+
else
|
|
88
|
+
raise Error, "Cannot allocate output pointer for type: #{type_sym}"
|
|
89
|
+
end
|
|
90
|
+
Fiddle::Pointer.malloc(size)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def read_output_values(out_refs)
|
|
94
|
+
out_refs.map do |ref|
|
|
95
|
+
value = case ref[:type]
|
|
96
|
+
when :char then ref[:ptr][0, Fiddle::SIZEOF_CHAR].unpack1('c')
|
|
97
|
+
when :uchar then ref[:ptr][0, Fiddle::SIZEOF_CHAR].unpack1('C')
|
|
98
|
+
when :short then ref[:ptr][0, Fiddle::SIZEOF_SHORT].unpack1('s')
|
|
99
|
+
when :ushort then ref[:ptr][0, Fiddle::SIZEOF_SHORT].unpack1('S')
|
|
100
|
+
when :int then ref[:ptr][0, Fiddle::SIZEOF_INT].unpack1('i')
|
|
101
|
+
when :uint then ref[:ptr][0, Fiddle::SIZEOF_INT].unpack1('I')
|
|
102
|
+
when :long then ref[:ptr][0, Fiddle::SIZEOF_LONG].unpack1('l!')
|
|
103
|
+
when :ulong then ref[:ptr][0, Fiddle::SIZEOF_LONG].unpack1('L!')
|
|
104
|
+
when :long_long then ref[:ptr][0, Fiddle::SIZEOF_LONG_LONG].unpack1('q')
|
|
105
|
+
when :ulong_long then ref[:ptr][0, Fiddle::SIZEOF_LONG_LONG].unpack1('Q')
|
|
106
|
+
when :float then ref[:ptr][0, Fiddle::SIZEOF_FLOAT].unpack1('f')
|
|
107
|
+
when :double then ref[:ptr][0, Fiddle::SIZEOF_DOUBLE].unpack1('d')
|
|
108
|
+
when :voidp then format('0x%x', ref[:ptr][0, Fiddle::SIZEOF_VOIDP].unpack1('J'))
|
|
109
|
+
else
|
|
110
|
+
raise Error, "Cannot read output value for type: #{ref[:type]}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
{ index: ref[:index], type: ref[:type].to_s, value: value }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
62
116
|
end
|
|
63
117
|
end
|
data/lib/libcall/cli.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'optparse'
|
|
4
3
|
require 'json'
|
|
5
4
|
|
|
6
5
|
module Libcall
|
|
6
|
+
# Command-line interface for calling C functions from shared libraries
|
|
7
7
|
class CLI
|
|
8
8
|
def initialize(argv)
|
|
9
9
|
@argv = argv
|
|
@@ -19,41 +19,31 @@ module Libcall
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def run
|
|
22
|
-
|
|
22
|
+
lib_path, func_name, arg_pairs = scan_argv!(@argv)
|
|
23
23
|
|
|
24
|
-
if
|
|
25
|
-
puts @parser.help
|
|
26
|
-
exit 1
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Resolve library path
|
|
24
|
+
# Resolve library path if a library name (-l) was given
|
|
30
25
|
if @options[:lib_name]
|
|
31
26
|
finder = LibraryFinder.new(lib_paths: @options[:lib_paths])
|
|
32
27
|
lib_path = finder.find(@options[:lib_name])
|
|
33
|
-
else
|
|
34
|
-
lib_path = @options[:lib] || @argv.shift
|
|
35
28
|
end
|
|
36
29
|
|
|
37
|
-
func_name = @argv.shift
|
|
38
|
-
args = @argv
|
|
39
|
-
|
|
40
30
|
if lib_path.nil? || func_name.nil?
|
|
41
31
|
warn 'Error: Missing required arguments'
|
|
42
|
-
warn 'Usage: libcall <LIBRARY> <FUNCTION>
|
|
32
|
+
warn 'Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...'
|
|
43
33
|
exit 1
|
|
44
34
|
end
|
|
45
35
|
|
|
46
36
|
if @options[:verbose]
|
|
47
37
|
warn "Library: #{lib_path}"
|
|
48
38
|
warn "Function: #{func_name}"
|
|
49
|
-
warn "Arguments: #{
|
|
39
|
+
warn "Arguments: #{arg_pairs.inspect}"
|
|
50
40
|
warn "Return type: #{@options[:return_type]}"
|
|
51
41
|
end
|
|
52
42
|
|
|
53
43
|
if @options[:dry_run]
|
|
54
|
-
dry_run_info(lib_path, func_name,
|
|
44
|
+
dry_run_info(lib_path, func_name, arg_pairs)
|
|
55
45
|
else
|
|
56
|
-
execute_call(lib_path, func_name,
|
|
46
|
+
execute_call(lib_path, func_name, arg_pairs)
|
|
57
47
|
end
|
|
58
48
|
rescue Error => e
|
|
59
49
|
warn "Error: #{e.message}"
|
|
@@ -66,113 +56,218 @@ module Libcall
|
|
|
66
56
|
|
|
67
57
|
private
|
|
68
58
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> [ARGS...]
|
|
59
|
+
def parse_options_banner
|
|
60
|
+
<<~BANNER
|
|
61
|
+
Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
|
|
73
62
|
|
|
74
|
-
|
|
63
|
+
Call C functions in shared libraries from the command line.
|
|
75
64
|
|
|
76
|
-
|
|
77
|
-
libcall /lib/libm.so.6 sqrt 16.0f64 -r f64
|
|
78
|
-
libcall -lm sqrt 16.0f64 -r f64
|
|
79
|
-
libcall -lsum -L. add 10i32 20i32 -r i32
|
|
80
|
-
libcall --dry-run ./mylib.so test 42u64 -r void
|
|
65
|
+
Pass arguments as TYPE VALUE pairs only.
|
|
81
66
|
|
|
82
|
-
|
|
83
|
-
|
|
67
|
+
Examples:
|
|
68
|
+
libcall -lm -r f64 sqrt double 16
|
|
69
|
+
libcall -ltest -L ./build add_i32 int 10 int -23 -r int
|
|
70
|
+
libcall --dry-run ./mylib.so test u64 42 -r void
|
|
84
71
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
72
|
+
Options:
|
|
73
|
+
BANNER
|
|
74
|
+
end
|
|
88
75
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
76
|
+
# Custom scanner that supports:
|
|
77
|
+
# - Known flags anywhere (before/after function name)
|
|
78
|
+
# - Negative numbers as values (not mistaken for options)
|
|
79
|
+
# - TYPE VALUE pairs
|
|
80
|
+
def scan_argv!(argv)
|
|
81
|
+
lib_path = nil
|
|
82
|
+
func_name = nil
|
|
83
|
+
arg_pairs = []
|
|
84
|
+
|
|
85
|
+
positional_only = false
|
|
86
|
+
i = 0
|
|
87
|
+
while i < argv.length
|
|
88
|
+
tok = argv[i]
|
|
92
89
|
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
# End-of-options marker: switch to positional-only mode
|
|
91
|
+
if tok == '--'
|
|
92
|
+
positional_only = true
|
|
93
|
+
i += 1
|
|
94
|
+
next
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
# Try to handle as a known option (only if not in positional-only mode)
|
|
98
|
+
unless positional_only
|
|
99
|
+
option_consumed = handle_option!(tok, argv, i)
|
|
100
|
+
if option_consumed > 0
|
|
101
|
+
i += option_consumed
|
|
102
|
+
next
|
|
103
|
+
end
|
|
99
104
|
end
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
# Positional resolution for <LIBRARY> and <FUNCTION>
|
|
107
|
+
if lib_path.nil? && @options[:lib_name].nil?
|
|
108
|
+
lib_path = tok
|
|
109
|
+
i += 1
|
|
110
|
+
next
|
|
103
111
|
end
|
|
104
112
|
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
if func_name.nil?
|
|
114
|
+
func_name = tok
|
|
115
|
+
i += 1
|
|
116
|
+
next
|
|
107
117
|
end
|
|
108
118
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
# After function name: parse TYPE VALUE pairs (or TYPE-only for out:TYPE)
|
|
120
|
+
type_tok = tok
|
|
121
|
+
i += 1
|
|
122
|
+
|
|
123
|
+
type_sym = Parser.parse_type(type_tok)
|
|
124
|
+
|
|
125
|
+
# TYPE that represents an output pointer does not require a value
|
|
126
|
+
if type_sym.is_a?(Array) && type_sym.first == :out
|
|
127
|
+
arg_pairs << [type_sym, nil]
|
|
128
|
+
next
|
|
112
129
|
end
|
|
113
130
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
# Allow `--` between TYPE and VALUE to switch to positional-only
|
|
132
|
+
while i < argv.length && argv[i] == '--'
|
|
133
|
+
positional_only = true
|
|
134
|
+
i += 1
|
|
117
135
|
end
|
|
136
|
+
|
|
137
|
+
raise Error, "Missing value for argument of type #{type_tok}" if i >= argv.length
|
|
138
|
+
|
|
139
|
+
value_tok = argv[i]
|
|
140
|
+
value = Parser.coerce_value(type_sym, value_tok)
|
|
141
|
+
arg_pairs << [type_sym, value]
|
|
142
|
+
i += 1
|
|
118
143
|
end
|
|
119
144
|
|
|
120
|
-
|
|
145
|
+
[lib_path, func_name, arg_pairs]
|
|
121
146
|
end
|
|
122
147
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
# Handle known option flags and return number of consumed tokens (0 if not an option)
|
|
149
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
150
|
+
def handle_option!(tok, argv, i)
|
|
151
|
+
case tok
|
|
152
|
+
when '--dry-run'
|
|
153
|
+
@options[:dry_run] = true
|
|
154
|
+
1
|
|
155
|
+
when '--json'
|
|
156
|
+
@options[:json] = true
|
|
157
|
+
1
|
|
158
|
+
when '--verbose'
|
|
159
|
+
@options[:verbose] = true
|
|
160
|
+
1
|
|
161
|
+
when '-h', '--help'
|
|
162
|
+
puts parse_options_banner
|
|
163
|
+
exit 0
|
|
164
|
+
when '-v', '--version'
|
|
165
|
+
puts "libcall #{Libcall::VERSION}"
|
|
166
|
+
exit 0
|
|
167
|
+
when '-l', '--lib'
|
|
168
|
+
raise Error, 'Missing value for -l/--lib' if i + 1 >= argv.length
|
|
130
169
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
170
|
+
@options[:lib_name] = argv[i + 1]
|
|
171
|
+
2
|
|
172
|
+
when /\A-l(.+)\z/
|
|
173
|
+
@options[:lib_name] = ::Regexp.last_match(1)
|
|
174
|
+
1
|
|
175
|
+
when '-L', '--lib-path'
|
|
176
|
+
raise Error, 'Missing value for -L/--lib-path' if i + 1 >= argv.length
|
|
177
|
+
|
|
178
|
+
@options[:lib_paths] << argv[i + 1]
|
|
179
|
+
2
|
|
180
|
+
when '-r', '--ret'
|
|
181
|
+
raise Error, 'Missing value for -r/--ret' if i + 1 >= argv.length
|
|
182
|
+
|
|
183
|
+
@options[:return_type] = Parser.parse_return_type(argv[i + 1])
|
|
184
|
+
2
|
|
185
|
+
when /\A-r(.+)\z/
|
|
186
|
+
@options[:return_type] = Parser.parse_return_type(::Regexp.last_match(1))
|
|
187
|
+
1
|
|
188
|
+
else
|
|
189
|
+
0 # Not an option
|
|
139
190
|
end
|
|
191
|
+
end
|
|
192
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
193
|
+
|
|
194
|
+
def dry_run_info(lib_path, func_name, arg_pairs)
|
|
195
|
+
info = build_info_hash(lib_path, func_name, arg_pairs)
|
|
140
196
|
|
|
141
197
|
if @options[:json]
|
|
142
198
|
puts JSON.pretty_generate(info)
|
|
143
199
|
else
|
|
144
|
-
|
|
145
|
-
puts "Function: #{info[:function]}"
|
|
146
|
-
puts "Return: #{info[:return_type]}"
|
|
147
|
-
unless info[:arguments].empty?
|
|
148
|
-
puts 'Arguments:'
|
|
149
|
-
info[:arguments].each do |arg|
|
|
150
|
-
puts " [#{arg[:index]}] #{arg[:raw]} => #{arg[:type]} (#{arg[:value].inspect})"
|
|
151
|
-
end
|
|
152
|
-
end
|
|
200
|
+
print_info(info)
|
|
153
201
|
end
|
|
154
202
|
end
|
|
155
203
|
|
|
156
|
-
def
|
|
204
|
+
def build_info_hash(lib_path, func_name, arg_pairs)
|
|
205
|
+
{
|
|
206
|
+
library: lib_path,
|
|
207
|
+
function: func_name,
|
|
208
|
+
arguments: arg_pairs.map.with_index do |(type_sym, value), i|
|
|
209
|
+
{
|
|
210
|
+
index: i,
|
|
211
|
+
type: type_sym.to_s,
|
|
212
|
+
value: value
|
|
213
|
+
}
|
|
214
|
+
end,
|
|
215
|
+
return_type: @options[:return_type].to_s
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def print_info(info)
|
|
220
|
+
puts "Library: #{info[:library]}"
|
|
221
|
+
puts "Function: #{info[:function]}"
|
|
222
|
+
puts "Return: #{info[:return_type]}"
|
|
223
|
+
return if info[:arguments].empty?
|
|
224
|
+
|
|
225
|
+
puts 'Arguments:'
|
|
226
|
+
info[:arguments].each do |arg|
|
|
227
|
+
puts " [#{arg[:index]}] #{arg[:type]} = #{arg[:value].inspect}"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def execute_call(lib_path, func_name, arg_pairs)
|
|
157
232
|
caller = Caller.new(
|
|
158
233
|
lib_path,
|
|
159
234
|
func_name,
|
|
160
|
-
|
|
235
|
+
arg_pairs: arg_pairs,
|
|
161
236
|
return_type: @options[:return_type]
|
|
162
237
|
)
|
|
163
238
|
|
|
164
239
|
result = caller.call
|
|
240
|
+
output_result(lib_path, func_name, result)
|
|
241
|
+
end
|
|
165
242
|
|
|
243
|
+
def output_result(lib_path, func_name, result)
|
|
166
244
|
if @options[:json]
|
|
167
245
|
output = {
|
|
168
246
|
library: lib_path,
|
|
169
247
|
function: func_name,
|
|
170
|
-
return_type: @options[:return_type].to_s
|
|
171
|
-
result: result
|
|
248
|
+
return_type: @options[:return_type].to_s
|
|
172
249
|
}
|
|
250
|
+
|
|
251
|
+
if result.is_a?(Hash) && result.key?(:outputs)
|
|
252
|
+
output[:result] = result[:result]
|
|
253
|
+
output[:outputs] = result[:outputs]
|
|
254
|
+
else
|
|
255
|
+
output[:result] = result
|
|
256
|
+
end
|
|
257
|
+
|
|
173
258
|
puts JSON.pretty_generate(output, allow_nan: true)
|
|
174
259
|
else
|
|
175
|
-
|
|
260
|
+
if result.is_a?(Hash) && result.key?(:outputs)
|
|
261
|
+
puts "Result: #{result[:result]}" unless result[:result].nil?
|
|
262
|
+
unless result[:outputs].empty?
|
|
263
|
+
puts 'Output parameters:'
|
|
264
|
+
result[:outputs].each do |out|
|
|
265
|
+
puts " [#{out[:index]}] #{out[:type]} = #{out[:value]}"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
else
|
|
269
|
+
puts result unless result.nil?
|
|
270
|
+
end
|
|
176
271
|
end
|
|
177
272
|
end
|
|
178
273
|
end
|
|
@@ -9,6 +9,7 @@ rescue LoadError
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
module Libcall
|
|
12
|
+
# Find shared libraries by name using standard search paths and pkg-config
|
|
12
13
|
class LibraryFinder
|
|
13
14
|
def initialize(lib_paths: [])
|
|
14
15
|
@lib_paths = lib_paths
|
|
@@ -64,31 +65,48 @@ module Libcall
|
|
|
64
65
|
def default_library_paths
|
|
65
66
|
paths = []
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
paths << '/usr/lib/aarch64-linux-gnu'
|
|
79
|
-
end
|
|
68
|
+
if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
|
69
|
+
# Windows paths
|
|
70
|
+
paths << 'C:/Windows/System32'
|
|
71
|
+
paths << 'C:/Windows/SysWOW64'
|
|
72
|
+
|
|
73
|
+
# MSYS2/MinGW paths
|
|
74
|
+
if ENV['MSYSTEM']
|
|
75
|
+
msys_prefix = ENV['MINGW_PREFIX'] || 'C:/msys64/mingw64'
|
|
76
|
+
paths << "#{msys_prefix}/bin"
|
|
77
|
+
paths << "#{msys_prefix}/lib"
|
|
78
|
+
end
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
# Add PATH directories on Windows
|
|
81
|
+
paths.concat(ENV['PATH'].split(';').map { |p| p.tr('\\', '/') }) if ENV['PATH']
|
|
82
|
+
else
|
|
83
|
+
# Unix-like systems (Linux, macOS)
|
|
84
|
+
# Standard library paths
|
|
85
|
+
paths << '/lib'
|
|
86
|
+
paths << '/usr/lib'
|
|
83
87
|
paths << '/usr/local/lib'
|
|
84
|
-
paths << '/opt/homebrew/lib'
|
|
85
|
-
end
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
# Architecture-specific paths
|
|
90
|
+
if RUBY_PLATFORM =~ /x86_64/
|
|
91
|
+
paths << '/lib/x86_64-linux-gnu'
|
|
92
|
+
paths << '/usr/lib/x86_64-linux-gnu'
|
|
93
|
+
elsif RUBY_PLATFORM =~ /aarch64|arm64/
|
|
94
|
+
paths << '/lib/aarch64-linux-gnu'
|
|
95
|
+
paths << '/usr/lib/aarch64-linux-gnu'
|
|
96
|
+
end
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
# macOS paths
|
|
99
|
+
if RUBY_PLATFORM =~ /darwin/
|
|
100
|
+
paths << '/usr/local/lib'
|
|
101
|
+
paths << '/opt/homebrew/lib'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# LD_LIBRARY_PATH
|
|
105
|
+
paths.concat(ENV['LD_LIBRARY_PATH'].split(':')) if ENV['LD_LIBRARY_PATH']
|
|
106
|
+
|
|
107
|
+
# DYLD_LIBRARY_PATH (macOS)
|
|
108
|
+
paths.concat(ENV['DYLD_LIBRARY_PATH'].split(':')) if ENV['DYLD_LIBRARY_PATH']
|
|
109
|
+
end
|
|
92
110
|
|
|
93
111
|
paths.select { |p| Dir.exist?(p) }
|
|
94
112
|
end
|
|
@@ -101,8 +119,15 @@ module Libcall
|
|
|
101
119
|
end
|
|
102
120
|
|
|
103
121
|
# Try with lib prefix and common extensions
|
|
104
|
-
extensions =
|
|
105
|
-
|
|
122
|
+
extensions = if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
|
123
|
+
['', '.dll', '.so', '.a']
|
|
124
|
+
elsif RUBY_PLATFORM =~ /darwin/
|
|
125
|
+
['', '.dylib', '.so', '.a']
|
|
126
|
+
else
|
|
127
|
+
['', '.so', '.a']
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
prefixes = lib_name.start_with?('lib') ? [''] : ['lib', '']
|
|
106
131
|
|
|
107
132
|
prefixes.each do |prefix|
|
|
108
133
|
extensions.each do |ext|
|
data/lib/libcall/parser.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Libcall
|
|
4
|
+
# Parse and coerce TYPE VALUE argument pairs for FFI calls
|
|
4
5
|
class Parser
|
|
5
6
|
TYPE_MAP = {
|
|
6
7
|
'i8' => :char,
|
|
@@ -17,7 +18,9 @@ module Libcall
|
|
|
17
18
|
'f64' => :double,
|
|
18
19
|
'cstr' => :string,
|
|
19
20
|
'ptr' => :voidp,
|
|
21
|
+
'pointer' => :voidp,
|
|
20
22
|
'void' => :void,
|
|
23
|
+
# Common aliases
|
|
21
24
|
'int' => :int,
|
|
22
25
|
'uint' => :uint,
|
|
23
26
|
'long' => :long,
|
|
@@ -25,40 +28,27 @@ module Libcall
|
|
|
25
28
|
'float' => :float,
|
|
26
29
|
'double' => :double,
|
|
27
30
|
'char' => :char,
|
|
28
|
-
'str' => :string
|
|
31
|
+
'str' => :string,
|
|
32
|
+
'string' => :string
|
|
29
33
|
}.freeze
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
INTEGER_TYPES = %i[int uint long ulong long_long ulong_long char uchar short ushort].freeze
|
|
36
|
+
FLOAT_TYPES = %i[float double].freeze
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
# Pair-only API helpers
|
|
39
|
+
def self.parse_type(type_str)
|
|
40
|
+
# Output pointer spec: out:TYPE (e.g., out:int, out:f64)
|
|
41
|
+
if type_str.start_with?('out:')
|
|
42
|
+
inner = type_str.sub(/^out:/, '')
|
|
43
|
+
inner_sym = TYPE_MAP[inner]
|
|
44
|
+
raise Error, "Unknown type in out: #{inner}" unless inner_sym
|
|
45
|
+
return [:out, inner_sym]
|
|
37
46
|
end
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
type_str = ::Regexp.last_match(2)
|
|
42
|
-
|
|
43
|
-
type_sym = TYPE_MAP[type_str]
|
|
44
|
-
raise Error, "Unknown type suffix: #{type_str}" unless type_sym
|
|
45
|
-
|
|
46
|
-
value = if %i[float double].include?(type_sym)
|
|
47
|
-
value_str.to_f
|
|
48
|
-
else
|
|
49
|
-
value_str.to_i
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
return [type_sym, value]
|
|
53
|
-
end
|
|
48
|
+
type_sym = TYPE_MAP[type_str]
|
|
49
|
+
raise Error, "Unknown type: #{type_str}" unless type_sym
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
[:int, arg.to_i]
|
|
57
|
-
elsif arg =~ /^[-+]?(?:\d+\.\d*|\d*\.\d+)$/
|
|
58
|
-
[:double, arg.to_f]
|
|
59
|
-
else
|
|
60
|
-
raise Error, "Cannot parse argument: #{arg}"
|
|
61
|
-
end
|
|
51
|
+
type_sym
|
|
62
52
|
end
|
|
63
53
|
|
|
64
54
|
def self.parse_return_type(type_str)
|
|
@@ -70,17 +60,38 @@ module Libcall
|
|
|
70
60
|
type_sym
|
|
71
61
|
end
|
|
72
62
|
|
|
73
|
-
def self.
|
|
74
|
-
|
|
63
|
+
def self.coerce_value(type_sym, token)
|
|
64
|
+
case type_sym
|
|
65
|
+
when *FLOAT_TYPES
|
|
66
|
+
Float(token)
|
|
67
|
+
when *INTEGER_TYPES
|
|
68
|
+
Integer(token)
|
|
69
|
+
when :voidp
|
|
70
|
+
# Accept common null tokens for pointer types
|
|
71
|
+
return 0 if token =~ /\A(null|nil|NULL|0)\z/
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
Integer(token)
|
|
74
|
+
when :string
|
|
75
|
+
strip_quotes(token)
|
|
76
|
+
when :void
|
|
77
|
+
raise Error, 'void cannot be used as an argument type'
|
|
78
|
+
else
|
|
79
|
+
raise Error, "Unknown type for coercion: #{type_sym}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.strip_quotes(token)
|
|
84
|
+
if (token.start_with?('"') && token.end_with?('"')) || (token.start_with?("'") && token.end_with?("'"))
|
|
85
|
+
token[1...-1]
|
|
86
|
+
else
|
|
87
|
+
token
|
|
79
88
|
end
|
|
80
|
-
[ret_type, arg_types]
|
|
81
89
|
end
|
|
82
90
|
|
|
83
91
|
def self.fiddle_type(type_sym)
|
|
92
|
+
# Output parameters are passed as pointers
|
|
93
|
+
return Fiddle::TYPE_VOIDP if type_sym.is_a?(Array) && type_sym.first == :out
|
|
94
|
+
|
|
84
95
|
case type_sym
|
|
85
96
|
when :void then Fiddle::TYPE_VOID
|
|
86
97
|
when :char then Fiddle::TYPE_CHAR
|
data/lib/libcall/version.rb
CHANGED