libcall 0.0.0 → 0.0.2
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 +104 -28
- data/lib/libcall/caller.rb +58 -10
- data/lib/libcall/cli.rb +204 -78
- data/lib/libcall/library_finder.rb +77 -57
- data/lib/libcall/parser.rb +71 -70
- data/lib/libcall/platform.rb +43 -0
- data/lib/libcall/type_map.rb +209 -0
- data/lib/libcall/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b45f825f6989bd4673b982435ce1bad8ff1b1785785118ffa66c8bdad84b1d7c
|
|
4
|
+
data.tar.gz: 9a60c1d6d69b2ff7e70a2884b6dfbc8e2b6099c87df40558559fdffffff5c65c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed0cde569e1eea83480b744907d3397f6e7abae68b991993090312edc6ccf74d4cf973afbad2dd3402142eccf390a3f992cf2174ad016ea0009d5d8b91e104bd
|
|
7
|
+
data.tar.gz: 42571c69fd4e1487e17b592ec98afc321ed646e938d9d773a96c4e450f4762c9d49975f7de47a33ba5f747e2e2c1367da00d7a3233ff059a8ab5a70f43a3bb92
|
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,31 +12,51 @@ 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
|
-
#
|
|
28
|
-
libcall
|
|
29
|
-
# =>
|
|
30
|
+
# libc strlen
|
|
31
|
+
libcall -lc strlen string "hello" -r usize
|
|
32
|
+
# => 5
|
|
33
|
+
```
|
|
34
|
+
|
|
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:
|
|
43
|
+
|
|
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
|
|
30
51
|
```
|
|
31
52
|
|
|
32
|
-
|
|
53
|
+
End of options `--`:
|
|
33
54
|
|
|
34
|
-
Use
|
|
55
|
+
- Use `--` to stop option parsing if a value starts with `-`
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
|
|
57
|
+
```sh
|
|
58
|
+
libcall -lc getenv string -- -r -r cstr
|
|
59
|
+
```
|
|
39
60
|
|
|
40
61
|
### Options
|
|
41
62
|
|
|
@@ -48,40 +69,95 @@ Use Rust-style type suffixes:
|
|
|
48
69
|
- `-h, --help` - show help
|
|
49
70
|
- `-v, --version` - show version
|
|
50
71
|
|
|
72
|
+
Library search:
|
|
73
|
+
|
|
74
|
+
- `-L` adds search paths; `-l` resolves by name
|
|
75
|
+
- On Linux and macOS, `LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` are honored
|
|
76
|
+
|
|
51
77
|
### More Examples
|
|
52
78
|
|
|
53
79
|
```sh
|
|
54
80
|
# JSON output
|
|
55
|
-
libcall --json -lm sqrt 9.
|
|
81
|
+
libcall --json -lm sqrt double 9.0 -r f64
|
|
56
82
|
|
|
57
83
|
# Dry run
|
|
58
|
-
libcall --dry-run
|
|
84
|
+
libcall --dry-run -lc getpid -r int
|
|
85
|
+
|
|
86
|
+
# Output parameter with libm
|
|
87
|
+
libcall -lm modf double -3.14 out:double -r f64
|
|
59
88
|
|
|
60
|
-
#
|
|
61
|
-
libcall -
|
|
89
|
+
# TYPE/VALUE pairs with -r after function
|
|
90
|
+
libcall -lm fabs double -5.5 -r f64
|
|
91
|
+
# => 5.5
|
|
92
|
+
|
|
93
|
+
# Windows: calling C runtime functions
|
|
94
|
+
libcall msvcrt.dll sqrt double 16.0 -r f64
|
|
95
|
+
# => 4.0
|
|
96
|
+
|
|
97
|
+
# Windows: accessing environment variables
|
|
98
|
+
libcall msvcrt.dll getenv string "PATH" -r cstr
|
|
62
99
|
```
|
|
63
100
|
|
|
64
101
|
## Type Reference
|
|
65
102
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
| `i8`
|
|
69
|
-
| `u8`
|
|
70
|
-
| `i16`
|
|
71
|
-
| `u16`
|
|
72
|
-
| `i32`
|
|
73
|
-
| `u32`
|
|
74
|
-
| `i64`
|
|
75
|
-
| `u64`
|
|
76
|
-
| `f32`
|
|
77
|
-
| `f64`
|
|
103
|
+
| Short (suffix) | Formal (C) | Note/Range |
|
|
104
|
+
| -------------- | ----------------------------------- | ------------------ |
|
|
105
|
+
| `i8` | `char` (≈ `int8_t`) | -128 to 127 |
|
|
106
|
+
| `u8` | `unsigned char` (≈ `uint8_t`) | 0 to 255 |
|
|
107
|
+
| `i16` | `short` (≈ `int16_t`) | -32768 to 32767 |
|
|
108
|
+
| `u16` | `unsigned short` (≈ `uint16_t`) | 0 to 65535 |
|
|
109
|
+
| `i32` | `int` (≈ `int32_t`) | typical 32-bit int |
|
|
110
|
+
| `u32` | `unsigned int` (≈ `uint32_t`) | unsigned 32-bit |
|
|
111
|
+
| `i64` | `long long` (≈ `int64_t`) | 64-bit |
|
|
112
|
+
| `u64` | `unsigned long long` (≈ `uint64_t`) | 64-bit |
|
|
113
|
+
| `f32` | `float` | single precision |
|
|
114
|
+
| `f64` | `double` | double precision |
|
|
115
|
+
|
|
116
|
+
Also supported:
|
|
117
|
+
|
|
118
|
+
- `string`: C string argument (char\*)
|
|
119
|
+
- `cstr`: C string return (char\*)
|
|
120
|
+
- `ptr`/`pointer`: void\* pointer
|
|
78
121
|
|
|
79
122
|
## pkg-config Support
|
|
80
123
|
|
|
81
124
|
Set `PKG_CONFIG_PATH` and use package names with `-l`:
|
|
82
125
|
|
|
83
126
|
```sh
|
|
84
|
-
PKG_CONFIG_PATH=/path/to/pkgconfig libcall -lmypackage func
|
|
127
|
+
PKG_CONFIG_PATH=/path/to/pkgconfig libcall -lmypackage func i32 42 -r i32
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Output parameters (out:TYPE)
|
|
131
|
+
|
|
132
|
+
You can pass output pointers by specifying `out:TYPE`. The pointer is allocated automatically, passed to the function, and printed after the call.
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
# double frexp(double x, int* exp)
|
|
136
|
+
libcall -lm frexp double 8.0 out:int -r f64
|
|
137
|
+
|
|
138
|
+
# JSON includes an "outputs" array
|
|
139
|
+
libcall --json -lm frexp double 8.0 out:int -r f64
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Arrays
|
|
143
|
+
|
|
144
|
+
- Input arrays: `TYPE[]` takes a comma-separated value list.
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
# zlib (Linux/macOS): uLong crc32(uLong crc, const Bytef* buf, uInt len)
|
|
148
|
+
libcall -lz crc32 uint 0 uchar[] 104,101,108,108,111 uint 5 -r uint
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- Output arrays: `out:TYPE[N]` allocates N elements and prints them after the call.
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
# Linux (libc): ssize_t getrandom(void* buf, size_t buflen, unsigned int flags)
|
|
155
|
+
libcall -lc getrandom out:uchar[16] size_t 16 uint 0 -r long
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
# macOS (libSystem): void arc4random_buf(void* buf, size_t nbytes)
|
|
160
|
+
libcall -lSystem arc4random_buf out:uchar[16] size_t 16 -r void
|
|
85
161
|
```
|
|
86
162
|
|
|
87
163
|
## Warning
|
data/lib/libcall/caller.rb
CHANGED
|
@@ -3,35 +3,66 @@
|
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
arg_pairs.each_with_index do |(type_sym, value), idx|
|
|
23
|
+
arg_types << TypeMap.to_fiddle_type(type_sym)
|
|
24
|
+
|
|
25
|
+
if type_sym.is_a?(Array)
|
|
26
|
+
case type_sym.first
|
|
27
|
+
when :out
|
|
28
|
+
inner = type_sym[1]
|
|
29
|
+
ptr = TypeMap.allocate_output_pointer(inner)
|
|
30
|
+
out_refs << { index: idx, kind: :out, type: inner, ptr: ptr }
|
|
31
|
+
arg_values << ptr.to_i
|
|
32
|
+
when :array
|
|
33
|
+
base = type_sym[1]
|
|
34
|
+
values = Array(value)
|
|
35
|
+
ptr = TypeMap.allocate_array(base, values.length)
|
|
36
|
+
TypeMap.write_array(ptr, base, values)
|
|
37
|
+
arg_values << ptr.to_i
|
|
38
|
+
when :out_array
|
|
39
|
+
base = type_sym[1]
|
|
40
|
+
count = type_sym[2]
|
|
41
|
+
ptr = TypeMap.allocate_array(base, count)
|
|
42
|
+
out_refs << { index: idx, kind: :out_array, base: base, count: count, ptr: ptr }
|
|
43
|
+
arg_values << ptr.to_i
|
|
44
|
+
else
|
|
45
|
+
raise Error, "Unknown array/output form: #{type_sym.inspect}"
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
arg_values << value
|
|
49
|
+
end
|
|
24
50
|
end
|
|
25
51
|
|
|
26
|
-
ret_type =
|
|
52
|
+
ret_type = TypeMap.to_fiddle_type(return_type)
|
|
27
53
|
|
|
28
54
|
handle = Fiddle.dlopen(lib_path)
|
|
29
55
|
func_ptr = handle[func_name]
|
|
30
56
|
func = Fiddle::Function.new(func_ptr, arg_types, ret_type)
|
|
31
57
|
|
|
32
|
-
|
|
58
|
+
raw_result = func.call(*arg_values)
|
|
59
|
+
formatted_result = format_result(raw_result, return_type)
|
|
33
60
|
|
|
34
|
-
|
|
61
|
+
if out_refs.empty?
|
|
62
|
+
formatted_result
|
|
63
|
+
else
|
|
64
|
+
{ result: formatted_result, outputs: read_output_values(out_refs) }
|
|
65
|
+
end
|
|
35
66
|
rescue Fiddle::DLError => e
|
|
36
67
|
raise Error, "Failed to load library or function: #{e.message}"
|
|
37
68
|
end
|
|
@@ -59,5 +90,22 @@ module Libcall
|
|
|
59
90
|
result
|
|
60
91
|
end
|
|
61
92
|
end
|
|
93
|
+
|
|
94
|
+
def read_output_values(out_refs)
|
|
95
|
+
out_refs.map do |ref|
|
|
96
|
+
case ref[:kind]
|
|
97
|
+
when :out
|
|
98
|
+
value = TypeMap.read_output_pointer(ref[:ptr], ref[:type])
|
|
99
|
+
{ index: ref[:index], type: ref[:type].to_s, value: value }
|
|
100
|
+
when :out_array
|
|
101
|
+
base = ref[:base]
|
|
102
|
+
count = ref[:count]
|
|
103
|
+
values = TypeMap.read_array(ref[:ptr], base, count)
|
|
104
|
+
{ index: ref[:index], type: "#{base}[#{count}]", value: values }
|
|
105
|
+
else
|
|
106
|
+
raise Error, "Unknown out reference kind: #{ref[:kind]}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
62
110
|
end
|
|
63
111
|
end
|
data/lib/libcall/cli.rb
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'optparse'
|
|
4
3
|
require 'json'
|
|
4
|
+
require_relative 'platform'
|
|
5
5
|
|
|
6
6
|
module Libcall
|
|
7
|
+
# Command-line interface for calling C functions from shared libraries
|
|
7
8
|
class CLI
|
|
9
|
+
PLATFORM_EXAMPLES = {
|
|
10
|
+
windows: <<~EXAMPLES.chomp,
|
|
11
|
+
libcall -lmsvcrt puts string "Hello from libcall" -r int
|
|
12
|
+
libcall -lKernel32 GetTickCount -r uint32
|
|
13
|
+
libcall -lmsvcrt getenv string "PATH" -r string
|
|
14
|
+
EXAMPLES
|
|
15
|
+
darwin: <<~EXAMPLES.chomp,
|
|
16
|
+
libcall -lSystem getpid -r int
|
|
17
|
+
libcall -lSystem puts string "Hello from libcall" -r int
|
|
18
|
+
libcall -lSystem getenv string "PATH" -r string
|
|
19
|
+
EXAMPLES
|
|
20
|
+
unix: <<~EXAMPLES.chomp
|
|
21
|
+
libcall -lm sqrt double 16 -r double
|
|
22
|
+
libcall -lc getpid -r int
|
|
23
|
+
libcall -lc getenv string "PATH" -r string
|
|
24
|
+
EXAMPLES
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
8
27
|
def initialize(argv)
|
|
9
28
|
@argv = argv
|
|
10
29
|
@options = {
|
|
@@ -19,41 +38,31 @@ module Libcall
|
|
|
19
38
|
end
|
|
20
39
|
|
|
21
40
|
def run
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if @argv.empty?
|
|
25
|
-
puts @parser.help
|
|
26
|
-
exit 1
|
|
27
|
-
end
|
|
41
|
+
lib_path, func_name, arg_pairs = scan_argv!(@argv)
|
|
28
42
|
|
|
29
|
-
# Resolve library path
|
|
43
|
+
# Resolve library path if a library name (-l) was given
|
|
30
44
|
if @options[:lib_name]
|
|
31
45
|
finder = LibraryFinder.new(lib_paths: @options[:lib_paths])
|
|
32
46
|
lib_path = finder.find(@options[:lib_name])
|
|
33
|
-
else
|
|
34
|
-
lib_path = @options[:lib] || @argv.shift
|
|
35
47
|
end
|
|
36
48
|
|
|
37
|
-
func_name = @argv.shift
|
|
38
|
-
args = @argv
|
|
39
|
-
|
|
40
49
|
if lib_path.nil? || func_name.nil?
|
|
41
50
|
warn 'Error: Missing required arguments'
|
|
42
|
-
warn 'Usage: libcall <LIBRARY> <FUNCTION>
|
|
51
|
+
warn 'Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...'
|
|
43
52
|
exit 1
|
|
44
53
|
end
|
|
45
54
|
|
|
46
55
|
if @options[:verbose]
|
|
47
56
|
warn "Library: #{lib_path}"
|
|
48
57
|
warn "Function: #{func_name}"
|
|
49
|
-
warn "Arguments: #{
|
|
58
|
+
warn "Arguments: #{arg_pairs.inspect}"
|
|
50
59
|
warn "Return type: #{@options[:return_type]}"
|
|
51
60
|
end
|
|
52
61
|
|
|
53
62
|
if @options[:dry_run]
|
|
54
|
-
dry_run_info(lib_path, func_name,
|
|
63
|
+
dry_run_info(lib_path, func_name, arg_pairs)
|
|
55
64
|
else
|
|
56
|
-
execute_call(lib_path, func_name,
|
|
65
|
+
execute_call(lib_path, func_name, arg_pairs)
|
|
57
66
|
end
|
|
58
67
|
rescue Error => e
|
|
59
68
|
warn "Error: #{e.message}"
|
|
@@ -66,111 +75,228 @@ module Libcall
|
|
|
66
75
|
|
|
67
76
|
private
|
|
68
77
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
def parse_options_banner
|
|
79
|
+
examples = if Platform.windows?
|
|
80
|
+
PLATFORM_EXAMPLES[:windows]
|
|
81
|
+
elsif Platform.darwin?
|
|
82
|
+
PLATFORM_EXAMPLES[:darwin]
|
|
83
|
+
else
|
|
84
|
+
PLATFORM_EXAMPLES[:unix]
|
|
85
|
+
end
|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
<<~BANNER
|
|
88
|
+
Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
|
|
75
89
|
|
|
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
|
|
90
|
+
Call C functions in shared libraries from the command line.
|
|
81
91
|
|
|
82
|
-
|
|
83
|
-
BANNER
|
|
92
|
+
Arguments are passed as TYPE VALUE pairs.
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
end
|
|
94
|
+
Examples:
|
|
95
|
+
#{examples.lines.map { |line| line.chomp }.join("\n ")}
|
|
88
96
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
Options:
|
|
98
|
+
-l, --lib LIBRARY Library name to search for (e.g., -lm for libm)
|
|
99
|
+
-L, --lib-path PATH Add directory to library search path
|
|
100
|
+
-r, --ret TYPE Return type (default: void)
|
|
101
|
+
--dry-run Show what would be executed without calling
|
|
102
|
+
--json Output result in JSON format
|
|
103
|
+
--verbose Show detailed information
|
|
104
|
+
-h, --help Show this help message
|
|
105
|
+
-v, --version Show version information
|
|
106
|
+
BANNER
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Custom scanner that supports:
|
|
110
|
+
# - Known flags anywhere (before/after function name)
|
|
111
|
+
# - Negative numbers as values (not mistaken for options)
|
|
112
|
+
# - TYPE VALUE pairs
|
|
113
|
+
def scan_argv!(argv)
|
|
114
|
+
lib_path = nil
|
|
115
|
+
func_name = nil
|
|
116
|
+
arg_pairs = []
|
|
117
|
+
|
|
118
|
+
positional_only = false
|
|
119
|
+
i = 0
|
|
120
|
+
while i < argv.length
|
|
121
|
+
tok = argv[i]
|
|
92
122
|
|
|
93
|
-
|
|
94
|
-
|
|
123
|
+
# End-of-options marker: switch to positional-only mode
|
|
124
|
+
if tok == '--'
|
|
125
|
+
positional_only = true
|
|
126
|
+
i += 1
|
|
127
|
+
next
|
|
95
128
|
end
|
|
96
129
|
|
|
97
|
-
|
|
98
|
-
|
|
130
|
+
# Try to handle as a known option (only if not in positional-only mode)
|
|
131
|
+
unless positional_only
|
|
132
|
+
option_consumed = handle_option!(tok, argv, i)
|
|
133
|
+
if option_consumed > 0
|
|
134
|
+
i += option_consumed
|
|
135
|
+
next
|
|
136
|
+
end
|
|
99
137
|
end
|
|
100
138
|
|
|
101
|
-
|
|
102
|
-
|
|
139
|
+
# Positional resolution for <LIBRARY> and <FUNCTION>
|
|
140
|
+
if lib_path.nil? && @options[:lib_name].nil?
|
|
141
|
+
lib_path = tok
|
|
142
|
+
i += 1
|
|
143
|
+
next
|
|
103
144
|
end
|
|
104
145
|
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
if func_name.nil?
|
|
147
|
+
func_name = tok
|
|
148
|
+
i += 1
|
|
149
|
+
next
|
|
107
150
|
end
|
|
108
151
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
152
|
+
# After function name: parse TYPE VALUE pairs (or TYPE-only for out:TYPE)
|
|
153
|
+
type_tok = tok
|
|
154
|
+
i += 1
|
|
155
|
+
|
|
156
|
+
type_sym = Parser.parse_type(type_tok)
|
|
157
|
+
|
|
158
|
+
# TYPE that represents an output pointer/array does not require a value
|
|
159
|
+
if type_sym.is_a?(Array) && %i[out out_array].include?(type_sym.first)
|
|
160
|
+
arg_pairs << [type_sym, nil]
|
|
161
|
+
next
|
|
112
162
|
end
|
|
113
163
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
164
|
+
# Allow `--` between TYPE and VALUE to switch to positional-only
|
|
165
|
+
while i < argv.length && argv[i] == '--'
|
|
166
|
+
positional_only = true
|
|
167
|
+
i += 1
|
|
117
168
|
end
|
|
169
|
+
|
|
170
|
+
raise Error, "Missing value for argument of type #{type_tok}" if i >= argv.length
|
|
171
|
+
|
|
172
|
+
value_tok = argv[i]
|
|
173
|
+
value = Parser.coerce_value(type_sym, value_tok)
|
|
174
|
+
arg_pairs << [type_sym, value]
|
|
175
|
+
i += 1
|
|
118
176
|
end
|
|
119
177
|
|
|
120
|
-
|
|
178
|
+
[lib_path, func_name, arg_pairs]
|
|
121
179
|
end
|
|
122
180
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
181
|
+
# Handle known option flags and return number of consumed tokens (0 if not an option)
|
|
182
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
183
|
+
def handle_option!(tok, argv, i)
|
|
184
|
+
case tok
|
|
185
|
+
when '--dry-run'
|
|
186
|
+
@options[:dry_run] = true
|
|
187
|
+
1
|
|
188
|
+
when '--json'
|
|
189
|
+
@options[:json] = true
|
|
190
|
+
1
|
|
191
|
+
when '--verbose'
|
|
192
|
+
@options[:verbose] = true
|
|
193
|
+
1
|
|
194
|
+
when '-h', '--help'
|
|
195
|
+
puts parse_options_banner
|
|
196
|
+
exit 0
|
|
197
|
+
when '-v', '--version'
|
|
198
|
+
puts "libcall #{Libcall::VERSION}"
|
|
199
|
+
exit 0
|
|
200
|
+
when '-l', '--lib'
|
|
201
|
+
raise Error, 'Missing value for -l/--lib' if i + 1 >= argv.length
|
|
130
202
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
203
|
+
@options[:lib_name] = argv[i + 1]
|
|
204
|
+
2
|
|
205
|
+
when /\A-l(.+)\z/
|
|
206
|
+
@options[:lib_name] = ::Regexp.last_match(1)
|
|
207
|
+
1
|
|
208
|
+
when '-L', '--lib-path'
|
|
209
|
+
raise Error, 'Missing value for -L/--lib-path' if i + 1 >= argv.length
|
|
210
|
+
|
|
211
|
+
@options[:lib_paths] << argv[i + 1]
|
|
212
|
+
2
|
|
213
|
+
when '-r', '--ret'
|
|
214
|
+
raise Error, 'Missing value for -r/--ret' if i + 1 >= argv.length
|
|
215
|
+
|
|
216
|
+
@options[:return_type] = Parser.parse_return_type(argv[i + 1])
|
|
217
|
+
2
|
|
218
|
+
when /\A-r(.+)\z/
|
|
219
|
+
@options[:return_type] = Parser.parse_return_type(::Regexp.last_match(1))
|
|
220
|
+
1
|
|
221
|
+
else
|
|
222
|
+
0 # Not an option
|
|
139
223
|
end
|
|
224
|
+
end
|
|
225
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
|
|
226
|
+
|
|
227
|
+
def dry_run_info(lib_path, func_name, arg_pairs)
|
|
228
|
+
info = build_info_hash(lib_path, func_name, arg_pairs)
|
|
140
229
|
|
|
141
230
|
if @options[:json]
|
|
142
231
|
puts JSON.pretty_generate(info)
|
|
143
232
|
else
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
233
|
+
print_info(info)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def build_info_hash(lib_path, func_name, arg_pairs)
|
|
238
|
+
{
|
|
239
|
+
library: lib_path,
|
|
240
|
+
function: func_name,
|
|
241
|
+
arguments: arg_pairs.map.with_index do |(type_sym, value), i|
|
|
242
|
+
{
|
|
243
|
+
index: i,
|
|
244
|
+
type: type_sym.to_s,
|
|
245
|
+
value: value
|
|
246
|
+
}
|
|
247
|
+
end,
|
|
248
|
+
return_type: @options[:return_type].to_s
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def print_info(info)
|
|
253
|
+
puts "Library: #{info[:library]}"
|
|
254
|
+
puts "Function: #{info[:function]}"
|
|
255
|
+
puts "Return: #{info[:return_type]}"
|
|
256
|
+
return if info[:arguments].empty?
|
|
257
|
+
|
|
258
|
+
puts 'Arguments:'
|
|
259
|
+
info[:arguments].each do |arg|
|
|
260
|
+
puts " [#{arg[:index]}] #{arg[:type]} = #{arg[:value].inspect}"
|
|
153
261
|
end
|
|
154
262
|
end
|
|
155
263
|
|
|
156
|
-
def execute_call(lib_path, func_name,
|
|
264
|
+
def execute_call(lib_path, func_name, arg_pairs)
|
|
157
265
|
caller = Caller.new(
|
|
158
266
|
lib_path,
|
|
159
267
|
func_name,
|
|
160
|
-
|
|
268
|
+
arg_pairs: arg_pairs,
|
|
161
269
|
return_type: @options[:return_type]
|
|
162
270
|
)
|
|
163
271
|
|
|
164
272
|
result = caller.call
|
|
273
|
+
output_result(lib_path, func_name, result)
|
|
274
|
+
end
|
|
165
275
|
|
|
276
|
+
def output_result(lib_path, func_name, result)
|
|
166
277
|
if @options[:json]
|
|
167
278
|
output = {
|
|
168
279
|
library: lib_path,
|
|
169
280
|
function: func_name,
|
|
170
|
-
return_type: @options[:return_type].to_s
|
|
171
|
-
result: result
|
|
281
|
+
return_type: @options[:return_type].to_s
|
|
172
282
|
}
|
|
283
|
+
|
|
284
|
+
if result.is_a?(Hash) && result.key?(:outputs)
|
|
285
|
+
output[:result] = result[:result]
|
|
286
|
+
output[:outputs] = result[:outputs]
|
|
287
|
+
else
|
|
288
|
+
output[:result] = result
|
|
289
|
+
end
|
|
290
|
+
|
|
173
291
|
puts JSON.pretty_generate(output, allow_nan: true)
|
|
292
|
+
elsif result.is_a?(Hash) && result.key?(:outputs)
|
|
293
|
+
puts "Result: #{result[:result]}" unless result[:result].nil?
|
|
294
|
+
unless result[:outputs].empty?
|
|
295
|
+
puts 'Output parameters:'
|
|
296
|
+
result[:outputs].each do |out|
|
|
297
|
+
puts " [#{out[:index]}] #{out[:type]} = #{out[:value]}"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
174
300
|
else
|
|
175
301
|
puts result unless result.nil?
|
|
176
302
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'fiddle'
|
|
4
|
+
require_relative 'platform'
|
|
4
5
|
|
|
5
6
|
begin
|
|
6
7
|
require 'pkg-config'
|
|
@@ -9,6 +10,7 @@ rescue LoadError
|
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
module Libcall
|
|
13
|
+
# Find shared libraries by name using standard search paths and pkg-config
|
|
12
14
|
class LibraryFinder
|
|
13
15
|
def initialize(lib_paths: [])
|
|
14
16
|
@lib_paths = lib_paths
|
|
@@ -18,29 +20,23 @@ module Libcall
|
|
|
18
20
|
# Find library by name (e.g., "m" -> "/lib/x86_64-linux-gnu/libm.so.6")
|
|
19
21
|
def find(lib_name)
|
|
20
22
|
# If it's a path, return as-is
|
|
21
|
-
return File.expand_path(lib_name) if
|
|
22
|
-
return File.expand_path(lib_name) if File.file?(lib_name)
|
|
23
|
+
return File.expand_path(lib_name) if path_like?(lib_name)
|
|
23
24
|
|
|
24
25
|
search_paths = @lib_paths + @default_paths
|
|
25
26
|
|
|
26
27
|
if defined?(PKGConfig)
|
|
27
|
-
pkg_exists =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
pkg_exists = begin
|
|
29
|
+
PKGConfig.public_send(
|
|
30
|
+
PKGConfig.respond_to?(:exist?) ? :exist? : :have_package,
|
|
31
|
+
lib_name
|
|
32
|
+
)
|
|
33
|
+
rescue StandardError
|
|
34
|
+
false
|
|
35
|
+
end
|
|
32
36
|
|
|
33
37
|
if pkg_exists
|
|
34
|
-
lib_dirs =
|
|
35
|
-
|
|
36
|
-
else
|
|
37
|
-
PKGConfig.libs(lib_name).to_s.split.select { |t| t.start_with?('-L') }.map { |t| t[2..] }
|
|
38
|
-
end
|
|
39
|
-
lib_names = if PKGConfig.respond_to?(:libs_only_l)
|
|
40
|
-
PKGConfig.libs_only_l(lib_name).to_s.split.map { |l| l.start_with?('-l') ? l[2..] : l }
|
|
41
|
-
else
|
|
42
|
-
PKGConfig.libs(lib_name).to_s.split.select { |t| t.start_with?('-l') }.map { |t| t[2..] }
|
|
43
|
-
end
|
|
38
|
+
lib_dirs = extract_pkg_config_flags(lib_name, 'L')
|
|
39
|
+
lib_names = extract_pkg_config_flags(lib_name, 'l')
|
|
44
40
|
|
|
45
41
|
search_paths = lib_dirs + search_paths
|
|
46
42
|
|
|
@@ -61,36 +57,65 @@ module Libcall
|
|
|
61
57
|
|
|
62
58
|
private
|
|
63
59
|
|
|
60
|
+
def path_like?(name)
|
|
61
|
+
name.include?('/') || name.include?('\\') || File.file?(name)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Extract -L or -l flags from pkg-config output, normalized without the dash prefix
|
|
65
|
+
def extract_pkg_config_flags(lib_name, flag_char)
|
|
66
|
+
base = if flag_char == 'L' && PKGConfig.respond_to?(:libs_only_L)
|
|
67
|
+
PKGConfig.libs_only_L(lib_name)
|
|
68
|
+
elsif flag_char == 'l' && PKGConfig.respond_to?(:libs_only_l)
|
|
69
|
+
PKGConfig.libs_only_l(lib_name)
|
|
70
|
+
else
|
|
71
|
+
PKGConfig.libs(lib_name)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
base.to_s.split
|
|
75
|
+
.select { |t| t.start_with?("-#{flag_char}") }
|
|
76
|
+
.map { |t| t[2..] }
|
|
77
|
+
end
|
|
78
|
+
|
|
64
79
|
def default_library_paths
|
|
65
|
-
|
|
80
|
+
(Platform.windows? ? windows_library_paths : unix_library_paths)
|
|
81
|
+
.select { |p| Dir.exist?(p) }
|
|
82
|
+
end
|
|
66
83
|
|
|
67
|
-
|
|
68
|
-
paths
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
paths << '/lib/x86_64-linux-gnu'
|
|
75
|
-
paths << '/usr/lib/x86_64-linux-gnu'
|
|
76
|
-
elsif RUBY_PLATFORM =~ /aarch64|arm64/
|
|
77
|
-
paths << '/lib/aarch64-linux-gnu'
|
|
78
|
-
paths << '/usr/lib/aarch64-linux-gnu'
|
|
84
|
+
def windows_library_paths
|
|
85
|
+
paths = %w[C:/Windows/System32 C:/Windows/SysWOW64]
|
|
86
|
+
|
|
87
|
+
# MSYS2/MinGW paths
|
|
88
|
+
if ENV['MSYSTEM']
|
|
89
|
+
msys_prefix = ENV['MINGW_PREFIX'] || 'C:/msys64/mingw64'
|
|
90
|
+
paths.concat(["#{msys_prefix}/bin", "#{msys_prefix}/lib"])
|
|
79
91
|
end
|
|
80
92
|
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
# Add PATH directories on Windows
|
|
94
|
+
paths.concat(ENV['PATH'].split(';').map { |p| p.tr('\\', '/') }) if ENV['PATH']
|
|
95
|
+
|
|
96
|
+
paths
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def unix_library_paths
|
|
100
|
+
# Standard library paths
|
|
101
|
+
paths = %w[/lib /usr/lib /usr/local/lib]
|
|
102
|
+
|
|
103
|
+
# Architecture-specific paths (Linux)
|
|
104
|
+
case Platform.architecture
|
|
105
|
+
when 'x86_64'
|
|
106
|
+
paths.concat(%w[/lib/x86_64-linux-gnu /usr/lib/x86_64-linux-gnu])
|
|
107
|
+
when 'aarch64'
|
|
108
|
+
paths.concat(%w[/lib/aarch64-linux-gnu /usr/lib/aarch64-linux-gnu])
|
|
85
109
|
end
|
|
86
110
|
|
|
87
|
-
#
|
|
88
|
-
paths.concat(
|
|
111
|
+
# macOS paths
|
|
112
|
+
paths.concat(%w[/usr/local/lib /opt/homebrew/lib]) if Platform.darwin?
|
|
89
113
|
|
|
90
|
-
#
|
|
91
|
-
paths.concat(ENV
|
|
114
|
+
# Environment-based paths
|
|
115
|
+
paths.concat(ENV.fetch('LD_LIBRARY_PATH', '').split(':'))
|
|
116
|
+
paths.concat(ENV.fetch('DYLD_LIBRARY_PATH', '').split(':'))
|
|
92
117
|
|
|
93
|
-
paths
|
|
118
|
+
paths
|
|
94
119
|
end
|
|
95
120
|
|
|
96
121
|
def resolve_by_name_in_paths(lib_name, search_paths)
|
|
@@ -101,24 +126,19 @@ module Libcall
|
|
|
101
126
|
end
|
|
102
127
|
|
|
103
128
|
# Try with lib prefix and common extensions
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
prefixes.each do |prefix|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
pattern = File.join(path, "#{name}.*")
|
|
118
|
-
matches = Dir.glob(pattern).select { |f| File.file?(f) }
|
|
119
|
-
return File.expand_path(matches.first) unless matches.empty?
|
|
120
|
-
end
|
|
121
|
-
end
|
|
129
|
+
prefixes = lib_name.start_with?('lib') ? [''] : ['lib', '']
|
|
130
|
+
extensions = Platform.library_extensions
|
|
131
|
+
|
|
132
|
+
prefixes.product(extensions, search_paths).each do |prefix, ext, path|
|
|
133
|
+
name = "#{prefix}#{lib_name}#{ext}"
|
|
134
|
+
full_path = File.join(path, name)
|
|
135
|
+
return File.expand_path(full_path) if File.file?(full_path)
|
|
136
|
+
|
|
137
|
+
# Check for versioned libraries (libm.so.6, etc.)
|
|
138
|
+
next if ext.empty?
|
|
139
|
+
|
|
140
|
+
matches = Dir.glob(File.join(path, "#{name}.*")).select { |f| File.file?(f) }
|
|
141
|
+
return File.expand_path(matches.first) unless matches.empty?
|
|
122
142
|
end
|
|
123
143
|
|
|
124
144
|
nil
|
data/lib/libcall/parser.rb
CHANGED
|
@@ -1,100 +1,101 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'type_map'
|
|
4
|
+
|
|
3
5
|
module Libcall
|
|
6
|
+
# Parse and coerce TYPE VALUE argument pairs for FFI calls
|
|
4
7
|
class Parser
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
'usize' => :ulong,
|
|
16
|
-
'f32' => :float,
|
|
17
|
-
'f64' => :double,
|
|
18
|
-
'cstr' => :string,
|
|
19
|
-
'ptr' => :voidp,
|
|
20
|
-
'void' => :void,
|
|
21
|
-
'int' => :int,
|
|
22
|
-
'uint' => :uint,
|
|
23
|
-
'long' => :long,
|
|
24
|
-
'ulong' => :ulong,
|
|
25
|
-
'float' => :float,
|
|
26
|
-
'double' => :double,
|
|
27
|
-
'char' => :char,
|
|
28
|
-
'str' => :string
|
|
29
|
-
}.freeze
|
|
30
|
-
|
|
31
|
-
def self.parse_arg(arg)
|
|
32
|
-
return [:string, ''] if arg.empty?
|
|
33
|
-
|
|
34
|
-
if (arg.start_with?('"') && arg.end_with?('"')) ||
|
|
35
|
-
(arg.start_with?("'") && arg.end_with?("'"))
|
|
36
|
-
return [:string, arg[1...-1]]
|
|
8
|
+
# Pair-only API helpers
|
|
9
|
+
def self.parse_type(type_str)
|
|
10
|
+
# Output array spec: out:TYPE[N]
|
|
11
|
+
if type_str.start_with?('out:') && type_str.match(/^out:(.+)\[(\d+)\]$/)
|
|
12
|
+
base = Regexp.last_match(1)
|
|
13
|
+
count = Regexp.last_match(2).to_i
|
|
14
|
+
base_sym = TypeMap.lookup(base)
|
|
15
|
+
raise Error, "Unknown output array type: #{base}" unless base_sym
|
|
16
|
+
|
|
17
|
+
return [:out_array, base_sym, count]
|
|
37
18
|
end
|
|
38
19
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
20
|
+
# Input array spec: TYPE[] (value as comma-separated list)
|
|
21
|
+
if type_str.end_with?('[]')
|
|
22
|
+
base = type_str[0..-3]
|
|
23
|
+
base_sym = TypeMap.lookup(base)
|
|
24
|
+
raise Error, "Unknown array base type: #{base}" unless base_sym
|
|
42
25
|
|
|
43
|
-
|
|
44
|
-
|
|
26
|
+
return [:array, base_sym]
|
|
27
|
+
end
|
|
45
28
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
29
|
+
# Output pointer spec: out:TYPE (e.g., out:int, out:f64)
|
|
30
|
+
if type_str.start_with?('out:')
|
|
31
|
+
inner = type_str.sub(/^out:/, '')
|
|
32
|
+
inner_sym = TypeMap.lookup(inner)
|
|
33
|
+
raise Error, "Unknown type in out: #{inner}" unless inner_sym
|
|
51
34
|
|
|
52
|
-
return [
|
|
35
|
+
return [:out, inner_sym]
|
|
53
36
|
end
|
|
54
37
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
else
|
|
60
|
-
raise Error, "Cannot parse argument: #{arg}"
|
|
61
|
-
end
|
|
38
|
+
type_sym = TypeMap.lookup(type_str)
|
|
39
|
+
raise Error, "Unknown type: #{type_str}" unless type_sym
|
|
40
|
+
|
|
41
|
+
type_sym
|
|
62
42
|
end
|
|
63
43
|
|
|
64
44
|
def self.parse_return_type(type_str)
|
|
65
45
|
return :void if type_str.nil? || type_str.empty? || type_str == 'void'
|
|
66
46
|
|
|
67
|
-
type_sym =
|
|
47
|
+
type_sym = TypeMap.lookup(type_str)
|
|
68
48
|
raise Error, "Unknown return type: #{type_str}" unless type_sym
|
|
69
49
|
|
|
70
50
|
type_sym
|
|
71
51
|
end
|
|
72
52
|
|
|
73
|
-
def self.
|
|
74
|
-
|
|
53
|
+
def self.coerce_value(type_sym, token)
|
|
54
|
+
# Input array values: comma-separated
|
|
55
|
+
if type_sym.is_a?(Array) && type_sym.first == :array
|
|
56
|
+
base = type_sym[1]
|
|
57
|
+
return [] if token.nil? || token.empty?
|
|
58
|
+
|
|
59
|
+
return token.split(',').map { |t| coerce_single_value(base, t.strip) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
case type_sym
|
|
63
|
+
when *TypeMap::FLOAT_TYPES
|
|
64
|
+
Float(token)
|
|
65
|
+
when *TypeMap::INTEGER_TYPES
|
|
66
|
+
Integer(token)
|
|
67
|
+
when :voidp
|
|
68
|
+
# Accept common null tokens for pointer types
|
|
69
|
+
return 0 if token =~ /\A(null|nil|NULL|0)\z/
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
Integer(token)
|
|
72
|
+
when :string
|
|
73
|
+
strip_quotes(token)
|
|
74
|
+
when :void
|
|
75
|
+
raise Error, 'void cannot be used as an argument type'
|
|
76
|
+
else
|
|
77
|
+
raise Error, "Unknown type for coercion: #{type_sym}"
|
|
79
78
|
end
|
|
80
|
-
[ret_type, arg_types]
|
|
81
79
|
end
|
|
82
80
|
|
|
83
|
-
def self.
|
|
81
|
+
def self.coerce_single_value(type_sym, token)
|
|
84
82
|
case type_sym
|
|
85
|
-
when
|
|
86
|
-
|
|
87
|
-
when
|
|
88
|
-
|
|
89
|
-
when :
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
when *TypeMap::FLOAT_TYPES
|
|
84
|
+
Float(token)
|
|
85
|
+
when *TypeMap::INTEGER_TYPES
|
|
86
|
+
Integer(token)
|
|
87
|
+
when :string
|
|
88
|
+
strip_quotes(token)
|
|
89
|
+
else
|
|
90
|
+
raise Error, "Unknown element type for coercion: #{type_sym}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.strip_quotes(token)
|
|
95
|
+
if (token.start_with?('"') && token.end_with?('"')) || (token.start_with?("'") && token.end_with?("'"))
|
|
96
|
+
token[1...-1]
|
|
96
97
|
else
|
|
97
|
-
|
|
98
|
+
token
|
|
98
99
|
end
|
|
99
100
|
end
|
|
100
101
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Libcall
|
|
4
|
+
# Platform detection utilities
|
|
5
|
+
module Platform
|
|
6
|
+
# Check if running on Windows
|
|
7
|
+
def self.windows?
|
|
8
|
+
RUBY_PLATFORM =~ /mswin|mingw|cygwin/
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Check if running on macOS
|
|
12
|
+
def self.darwin?
|
|
13
|
+
RUBY_PLATFORM =~ /darwin/
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Check if running on Unix-like system (Linux, BSD, etc.)
|
|
17
|
+
def self.unix?
|
|
18
|
+
!windows?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get platform-specific library extensions
|
|
22
|
+
def self.library_extensions
|
|
23
|
+
if windows?
|
|
24
|
+
['', '.dll', '.so', '.a']
|
|
25
|
+
elsif darwin?
|
|
26
|
+
['', '.dylib', '.so', '.a']
|
|
27
|
+
else
|
|
28
|
+
['', '.so', '.a']
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get architecture string
|
|
33
|
+
def self.architecture
|
|
34
|
+
if RUBY_PLATFORM =~ /x86_64/
|
|
35
|
+
'x86_64'
|
|
36
|
+
elsif RUBY_PLATFORM =~ /aarch64|arm64/
|
|
37
|
+
'aarch64'
|
|
38
|
+
else
|
|
39
|
+
'unknown'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fiddle'
|
|
4
|
+
|
|
5
|
+
module Libcall
|
|
6
|
+
# Type mapping for FFI calls
|
|
7
|
+
module TypeMap
|
|
8
|
+
# Map from string type names to FFI type symbols
|
|
9
|
+
MAP = {
|
|
10
|
+
# Short type names (Rust-like)
|
|
11
|
+
'i8' => :char,
|
|
12
|
+
'u8' => :uchar,
|
|
13
|
+
'i16' => :short,
|
|
14
|
+
'u16' => :ushort,
|
|
15
|
+
'i32' => :int,
|
|
16
|
+
'u32' => :uint,
|
|
17
|
+
'i64' => :long_long,
|
|
18
|
+
'u64' => :ulong_long,
|
|
19
|
+
'isize' => :long,
|
|
20
|
+
'usize' => :ulong,
|
|
21
|
+
'f32' => :float,
|
|
22
|
+
'f64' => :double,
|
|
23
|
+
# Pointer types
|
|
24
|
+
'cstr' => :string,
|
|
25
|
+
'ptr' => :voidp,
|
|
26
|
+
'pointer' => :voidp,
|
|
27
|
+
'void' => :void,
|
|
28
|
+
# Common C type names
|
|
29
|
+
'char' => :char,
|
|
30
|
+
'short' => :short,
|
|
31
|
+
'ushort' => :ushort,
|
|
32
|
+
'int' => :int,
|
|
33
|
+
'uint' => :uint,
|
|
34
|
+
'long' => :long,
|
|
35
|
+
'ulong' => :ulong,
|
|
36
|
+
'float' => :float,
|
|
37
|
+
'double' => :double,
|
|
38
|
+
# Extended type names (stdint-like)
|
|
39
|
+
'int8' => :char,
|
|
40
|
+
'uint8' => :uchar,
|
|
41
|
+
'int16' => :short,
|
|
42
|
+
'uint16' => :ushort,
|
|
43
|
+
'int32' => :int,
|
|
44
|
+
'uint32' => :uint,
|
|
45
|
+
'int64' => :long_long,
|
|
46
|
+
'uint64' => :ulong_long,
|
|
47
|
+
'float32' => :float,
|
|
48
|
+
'float64' => :double,
|
|
49
|
+
# Size and pointer-sized integers
|
|
50
|
+
'size_t' => :ulong,
|
|
51
|
+
'ssize_t' => :long,
|
|
52
|
+
'intptr' => :long,
|
|
53
|
+
'uintptr' => :ulong,
|
|
54
|
+
'intptr_t' => :long,
|
|
55
|
+
'uintptr_t' => :ulong,
|
|
56
|
+
'ptrdiff_t' => :long,
|
|
57
|
+
# Boolean
|
|
58
|
+
'bool' => :int,
|
|
59
|
+
# String aliases
|
|
60
|
+
'str' => :string,
|
|
61
|
+
'string' => :string
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
# Integer type symbols
|
|
65
|
+
INTEGER_TYPES = %i[
|
|
66
|
+
int uint
|
|
67
|
+
long ulong
|
|
68
|
+
long_long ulong_long
|
|
69
|
+
char uchar
|
|
70
|
+
short ushort
|
|
71
|
+
].freeze
|
|
72
|
+
|
|
73
|
+
# Floating point type symbols
|
|
74
|
+
FLOAT_TYPES = %i[float double].freeze
|
|
75
|
+
|
|
76
|
+
# Look up FFI type symbol from string
|
|
77
|
+
def self.lookup(type_str)
|
|
78
|
+
MAP[type_str]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if type symbol is an integer type
|
|
82
|
+
def self.integer_type?(type_sym)
|
|
83
|
+
INTEGER_TYPES.include?(type_sym)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check if type symbol is a floating point type
|
|
87
|
+
def self.float_type?(type_sym)
|
|
88
|
+
FLOAT_TYPES.include?(type_sym)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Convert type symbol to Fiddle type constant
|
|
92
|
+
def self.to_fiddle_type(type_sym)
|
|
93
|
+
# Array and output parameters are passed as pointers
|
|
94
|
+
if type_sym.is_a?(Array)
|
|
95
|
+
tag = type_sym.first
|
|
96
|
+
return Fiddle::TYPE_VOIDP if %i[out array out_array].include?(tag)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
case type_sym
|
|
100
|
+
when :void then Fiddle::TYPE_VOID
|
|
101
|
+
when :char then Fiddle::TYPE_CHAR
|
|
102
|
+
when :uchar then Fiddle::TYPE_UCHAR
|
|
103
|
+
when :short then Fiddle::TYPE_SHORT
|
|
104
|
+
when :ushort then Fiddle::TYPE_USHORT
|
|
105
|
+
when :int, :uint then Fiddle::TYPE_INT
|
|
106
|
+
when :long, :ulong then Fiddle::TYPE_LONG
|
|
107
|
+
when :long_long, :ulong_long then Fiddle::TYPE_LONG_LONG
|
|
108
|
+
when :float then Fiddle::TYPE_FLOAT
|
|
109
|
+
when :double then Fiddle::TYPE_DOUBLE
|
|
110
|
+
when :voidp, :string then Fiddle::TYPE_VOIDP
|
|
111
|
+
else
|
|
112
|
+
raise Error, "Unknown Fiddle type: #{type_sym}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Get the size in bytes for a type symbol
|
|
117
|
+
def self.sizeof(type_sym)
|
|
118
|
+
case type_sym
|
|
119
|
+
when :char, :uchar then Fiddle::SIZEOF_CHAR
|
|
120
|
+
when :short, :ushort then Fiddle::SIZEOF_SHORT
|
|
121
|
+
when :int, :uint then Fiddle::SIZEOF_INT
|
|
122
|
+
when :long, :ulong then Fiddle::SIZEOF_LONG
|
|
123
|
+
when :long_long, :ulong_long then Fiddle::SIZEOF_LONG_LONG
|
|
124
|
+
when :float then Fiddle::SIZEOF_FLOAT
|
|
125
|
+
when :double then Fiddle::SIZEOF_DOUBLE
|
|
126
|
+
when :voidp, :string then Fiddle::SIZEOF_VOIDP
|
|
127
|
+
else
|
|
128
|
+
raise Error, "Cannot get size for type: #{type_sym}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Allocate a pointer for output parameter
|
|
133
|
+
def self.allocate_output_pointer(type_sym)
|
|
134
|
+
ptr = Fiddle::Pointer.malloc(sizeof(type_sym))
|
|
135
|
+
# For out:string, we pass char**. Initialize inner pointer to NULL for safety.
|
|
136
|
+
ptr[0, Fiddle::SIZEOF_VOIDP] = [0].pack('J') if type_sym == :string
|
|
137
|
+
ptr
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Read value from output pointer
|
|
141
|
+
def self.read_output_pointer(ptr, type_sym)
|
|
142
|
+
case type_sym
|
|
143
|
+
when :char then ptr[0, Fiddle::SIZEOF_CHAR].unpack1('c')
|
|
144
|
+
when :uchar then ptr[0, Fiddle::SIZEOF_CHAR].unpack1('C')
|
|
145
|
+
when :short then ptr[0, Fiddle::SIZEOF_SHORT].unpack1('s')
|
|
146
|
+
when :ushort then ptr[0, Fiddle::SIZEOF_SHORT].unpack1('S')
|
|
147
|
+
when :int then ptr[0, Fiddle::SIZEOF_INT].unpack1('i')
|
|
148
|
+
when :uint then ptr[0, Fiddle::SIZEOF_INT].unpack1('I')
|
|
149
|
+
when :long then ptr[0, Fiddle::SIZEOF_LONG].unpack1('l!')
|
|
150
|
+
when :ulong then ptr[0, Fiddle::SIZEOF_LONG].unpack1('L!')
|
|
151
|
+
when :long_long then ptr[0, Fiddle::SIZEOF_LONG_LONG].unpack1('q')
|
|
152
|
+
when :ulong_long then ptr[0, Fiddle::SIZEOF_LONG_LONG].unpack1('Q')
|
|
153
|
+
when :float then ptr[0, Fiddle::SIZEOF_FLOAT].unpack1('f')
|
|
154
|
+
when :double then ptr[0, Fiddle::SIZEOF_DOUBLE].unpack1('d')
|
|
155
|
+
when :string
|
|
156
|
+
addr = ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J')
|
|
157
|
+
return '(null)' if addr.zero?
|
|
158
|
+
|
|
159
|
+
begin
|
|
160
|
+
Fiddle::Pointer.new(addr).to_s
|
|
161
|
+
rescue StandardError
|
|
162
|
+
format('0x%x', addr)
|
|
163
|
+
end
|
|
164
|
+
when :voidp then format('0x%x', ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J'))
|
|
165
|
+
else
|
|
166
|
+
raise Error, "Cannot read output value for type: #{type_sym}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Allocate memory for an array of base type and count elements
|
|
171
|
+
def self.allocate_array(base_type, count)
|
|
172
|
+
Fiddle::Pointer.malloc(sizeof(base_type) * count)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def self.write_array(ptr, base_type, values)
|
|
176
|
+
return if values.nil? || values.empty?
|
|
177
|
+
|
|
178
|
+
bytes = sizeof(base_type) * values.length
|
|
179
|
+
ptr[0, bytes] = values.pack(pack_template(base_type) + values.length.to_s)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.read_array(ptr, base_type, count)
|
|
183
|
+
return [] if count <= 0
|
|
184
|
+
|
|
185
|
+
bytes = sizeof(base_type) * count
|
|
186
|
+
raw = ptr[0, bytes]
|
|
187
|
+
raw.unpack(pack_template(base_type) + count.to_s)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.pack_template(base_type)
|
|
191
|
+
case base_type
|
|
192
|
+
when :char then 'c'
|
|
193
|
+
when :uchar then 'C'
|
|
194
|
+
when :short then 's'
|
|
195
|
+
when :ushort then 'S'
|
|
196
|
+
when :int then 'i'
|
|
197
|
+
when :uint then 'I'
|
|
198
|
+
when :long then 'l!'
|
|
199
|
+
when :ulong then 'L!'
|
|
200
|
+
when :long_long then 'q'
|
|
201
|
+
when :ulong_long then 'Q'
|
|
202
|
+
when :float then 'f'
|
|
203
|
+
when :double then 'd'
|
|
204
|
+
else
|
|
205
|
+
raise Error, "Unsupported array base type: #{base_type}"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
data/lib/libcall/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: libcall
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kojix2
|
|
@@ -51,6 +51,8 @@ files:
|
|
|
51
51
|
- lib/libcall/cli.rb
|
|
52
52
|
- lib/libcall/library_finder.rb
|
|
53
53
|
- lib/libcall/parser.rb
|
|
54
|
+
- lib/libcall/platform.rb
|
|
55
|
+
- lib/libcall/type_map.rb
|
|
54
56
|
- lib/libcall/version.rb
|
|
55
57
|
homepage: https://github.com/kojix2/libcall
|
|
56
58
|
licenses:
|