libcall 0.0.1 → 0.0.3
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 +116 -34
- data/lib/libcall/caller.rb +74 -43
- data/lib/libcall/cli.rb +57 -17
- data/lib/libcall/fiddley.rb +156 -0
- data/lib/libcall/library_finder.rb +73 -78
- data/lib/libcall/parser.rb +77 -58
- data/lib/libcall/platform.rb +43 -0
- data/lib/libcall/type_map.rb +242 -0
- data/lib/libcall/version.rb +1 -1
- data/lib/libcall.rb +1 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db2f2234f00412d76e307fc12179b0a829953a08bd6aefa83d59bbab866e85f3
|
|
4
|
+
data.tar.gz: bb2cd9b42fb09fc6b6e5201ed98c16dae47258e9a1fb23ef76fc6a49efe17ab3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12bc58798f6130a316ba87e53a66d2806035a7fd8abbb6ba2fc71654d82e0f2210149152cf9876ca818a5241808a2c871021eca2c7264a2110cfe37199da2872
|
|
7
|
+
data.tar.gz: bb6f252e8a7a9aec0a353abec0ed9674b346bb1af3471768ca51fad040234024c6fc77b18a25d42c396a389a0063ac44c5675f54fa8bd66eed3b0c9055da8e34
|
data/README.md
CHANGED
|
@@ -23,13 +23,11 @@ libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
|
|
|
23
23
|
### Quick Examples
|
|
24
24
|
|
|
25
25
|
```sh
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
# => 4.0
|
|
26
|
+
libcall -lm sqrt double 16 -r double # => 4.0
|
|
27
|
+
```
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
libcall
|
|
32
|
-
# => 30
|
|
29
|
+
```sh
|
|
30
|
+
libcall -lc strlen string "hello" -r usize # => 5
|
|
33
31
|
```
|
|
34
32
|
|
|
35
33
|
### Argument Syntax
|
|
@@ -63,7 +61,6 @@ libcall -lc getenv string -- -r -r cstr
|
|
|
63
61
|
- `-l LIBRARY` - library name (searches standard paths)
|
|
64
62
|
- `-L PATH` - add library search path
|
|
65
63
|
- `-r TYPE` - return type (void, i32, f64, cstr, ptr)
|
|
66
|
-
- Options may appear before or after the function name.
|
|
67
64
|
- `--dry-run` - validate without executing
|
|
68
65
|
- `--json` - JSON output
|
|
69
66
|
- `--verbose` - detailed info
|
|
@@ -77,42 +74,68 @@ Library search:
|
|
|
77
74
|
|
|
78
75
|
### More Examples
|
|
79
76
|
|
|
77
|
+
TYPE/VALUE pairs with `-r` before function
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
libcall -lm -r double fabs double -5.5 # => 5.5
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Output parameter with libm
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
libcall -lm modf double -3.14 out:double -r f64
|
|
87
|
+
# Result: -0.14000000000000012
|
|
88
|
+
# Output parameters:
|
|
89
|
+
# [1] double = -3.0
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
JSON output
|
|
93
|
+
|
|
80
94
|
```sh
|
|
81
|
-
# JSON output
|
|
82
95
|
libcall --json -lm sqrt double 9.0 -r f64
|
|
96
|
+
# {
|
|
97
|
+
# "library": "/lib/x86_64-linux-gnu/libm.so",
|
|
98
|
+
# "function": "sqrt",
|
|
99
|
+
# "return_type": "double",
|
|
100
|
+
# "result": 3.0
|
|
101
|
+
# }
|
|
102
|
+
```
|
|
83
103
|
|
|
84
|
-
|
|
85
|
-
libcall --dry-run ./mylib.so test u64 42 -r void
|
|
104
|
+
Dry run
|
|
86
105
|
|
|
87
|
-
|
|
88
|
-
libcall -
|
|
106
|
+
```sh
|
|
107
|
+
libcall --dry-run -lc getpid -r int
|
|
108
|
+
# Library: /lib/x86_64-linux-gnu/libc.so
|
|
109
|
+
# Function: getpid
|
|
110
|
+
# Return: int
|
|
111
|
+
```
|
|
89
112
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
113
|
+
Windows: calling C runtime functions
|
|
114
|
+
|
|
115
|
+
```powershell
|
|
116
|
+
libcall msvcrt.dll sqrt double 16.0 -r f64 # => 4.0
|
|
117
|
+
```
|
|
93
118
|
|
|
94
|
-
|
|
95
|
-
libcall msvcrt.dll sqrt double 16.0 -r f64
|
|
96
|
-
# => 4.0
|
|
119
|
+
Windows: accessing environment variables
|
|
97
120
|
|
|
98
|
-
|
|
121
|
+
```powershell
|
|
99
122
|
libcall msvcrt.dll getenv string "PATH" -r cstr
|
|
100
123
|
```
|
|
101
124
|
|
|
102
125
|
## Type Reference
|
|
103
126
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
106
|
-
| `i8`
|
|
107
|
-
| `u8`
|
|
108
|
-
| `i16`
|
|
109
|
-
| `u16`
|
|
110
|
-
| `i32`
|
|
111
|
-
| `u32`
|
|
112
|
-
| `i64`
|
|
113
|
-
| `u64`
|
|
114
|
-
| `f32`
|
|
115
|
-
| `f64`
|
|
127
|
+
| Short (suffix) | Formal (C) | Note/Range |
|
|
128
|
+
| -------------- | ----------------------------------- | ------------------ |
|
|
129
|
+
| `i8` | `char` (≈ `int8_t`) | -128 to 127 |
|
|
130
|
+
| `u8` | `unsigned char` (≈ `uint8_t`) | 0 to 255 |
|
|
131
|
+
| `i16` | `short` (≈ `int16_t`) | -32768 to 32767 |
|
|
132
|
+
| `u16` | `unsigned short` (≈ `uint16_t`) | 0 to 65535 |
|
|
133
|
+
| `i32` | `int` (≈ `int32_t`) | typical 32-bit int |
|
|
134
|
+
| `u32` | `unsigned int` (≈ `uint32_t`) | unsigned 32-bit |
|
|
135
|
+
| `i64` | `long long` (≈ `int64_t`) | 64-bit |
|
|
136
|
+
| `u64` | `unsigned long long` (≈ `uint64_t`) | 64-bit |
|
|
137
|
+
| `f32` | `float` | single precision |
|
|
138
|
+
| `f64` | `double` | double precision |
|
|
116
139
|
|
|
117
140
|
Also supported:
|
|
118
141
|
|
|
@@ -120,6 +143,8 @@ Also supported:
|
|
|
120
143
|
- `cstr`: C string return (char\*)
|
|
121
144
|
- `ptr`/`pointer`: void\* pointer
|
|
122
145
|
|
|
146
|
+
See [type_map.rb](lib/libcall/type_map.rb) for all available type mappings.
|
|
147
|
+
|
|
123
148
|
## pkg-config Support
|
|
124
149
|
|
|
125
150
|
Set `PKG_CONFIG_PATH` and use package names with `-l`:
|
|
@@ -133,13 +158,70 @@ PKG_CONFIG_PATH=/path/to/pkgconfig libcall -lmypackage func i32 42 -r i32
|
|
|
133
158
|
You can pass output pointers by specifying `out:TYPE`. The pointer is allocated automatically, passed to the function, and printed after the call.
|
|
134
159
|
|
|
135
160
|
```sh
|
|
136
|
-
#
|
|
137
|
-
libcall -
|
|
161
|
+
# double frexp(double x, int* exp)
|
|
162
|
+
libcall -lm frexp double 8.0 out:int -r f64
|
|
138
163
|
|
|
139
164
|
# JSON includes an "outputs" array
|
|
140
|
-
libcall --json -
|
|
165
|
+
libcall --json -lm frexp double 8.0 out:int -r f64
|
|
141
166
|
```
|
|
142
167
|
|
|
168
|
+
## Arrays
|
|
169
|
+
|
|
170
|
+
- Input arrays: `TYPE[]` takes a comma-separated value list.
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
# zlib (Linux/macOS): uLong crc32(uLong crc, const Bytef* buf, uInt len)
|
|
174
|
+
libcall -lz crc32 uint 0 uchar[] 104,101,108,108,111 uint 5 -r uint
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- Output arrays: `out:TYPE[N]` allocates N elements and prints them after the call.
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
# Linux (libc): ssize_t getrandom(void* buf, size_t buflen, unsigned int flags)
|
|
181
|
+
libcall -lc getrandom out:uchar[16] size_t 16 uint 0 -r long
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
# macOS (libSystem): void arc4random_buf(void* buf, size_t nbytes)
|
|
186
|
+
libcall -lSystem arc4random_buf out:uchar[16] size_t 16 -r void
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Callbacks (experimental)
|
|
190
|
+
|
|
191
|
+
Pass a C function pointer via a Ruby callback. Use `func` or `callback` with a quoted spec:
|
|
192
|
+
|
|
193
|
+
- Syntax: `func 'RET(ARG,ARG,...){|a, b, ...| ruby_code }'` (alias: `callback ...`)
|
|
194
|
+
- Inside the block, helper methods from `Libcall::Fiddley::DSL` are available:
|
|
195
|
+
- `int(ptr)`, `double(ptr)`, `cstr(ptr)` read values from pointers
|
|
196
|
+
- `read(:type, ptr)` reads any supported type; `ptr(addr)` makes a pointer
|
|
197
|
+
|
|
198
|
+
Quick examples
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
# Fixture function: int32_t apply_i32(int32_t, int32_t, int32_t (*)(int32_t,int32_t))
|
|
202
|
+
libcall -ltest -L test/fixtures/libtest/build apply_i32 \
|
|
203
|
+
int 3 int 5 \
|
|
204
|
+
func 'int(int,int){|a,b| a + b}' \
|
|
205
|
+
-r i32
|
|
206
|
+
# => 8
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
# libc qsort: sort 4 ints ascending; use out:int[4] with an initializer so the result prints
|
|
211
|
+
libcall -lc qsort \
|
|
212
|
+
out:int[4] 4,2,3,1 \
|
|
213
|
+
size_t 4 \
|
|
214
|
+
size_t 4 \
|
|
215
|
+
callback 'int(void*,void*){|pa,pb| int(pa) <=> int(pb) }' \
|
|
216
|
+
-r void
|
|
217
|
+
# Output parameters:
|
|
218
|
+
# [0] int[4] = [1, 2, 3, 4]
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Notes
|
|
222
|
+
|
|
223
|
+
- Match the C signature exactly (types and arity). Blocks run in-process; exceptions abort the call.
|
|
224
|
+
|
|
143
225
|
## Warning
|
|
144
226
|
|
|
145
227
|
FFI calls are inherently unsafe. You must:
|
data/lib/libcall/caller.rb
CHANGED
|
@@ -18,21 +18,75 @@ module Libcall
|
|
|
18
18
|
arg_types = []
|
|
19
19
|
arg_values = []
|
|
20
20
|
out_refs = []
|
|
21
|
+
closures = []
|
|
21
22
|
|
|
22
23
|
arg_pairs.each_with_index do |(type_sym, value), idx|
|
|
23
|
-
arg_types <<
|
|
24
|
-
|
|
25
|
-
if type_sym.is_a?(Array)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
arg_types << TypeMap.to_fiddle_type(type_sym)
|
|
25
|
+
|
|
26
|
+
if type_sym.is_a?(Array)
|
|
27
|
+
case type_sym.first
|
|
28
|
+
when :out
|
|
29
|
+
inner = type_sym[1]
|
|
30
|
+
ptr = TypeMap.allocate_output_pointer(inner)
|
|
31
|
+
out_refs << { index: idx, kind: :out, type: inner, ptr: ptr }
|
|
32
|
+
arg_values << ptr.to_i
|
|
33
|
+
when :array
|
|
34
|
+
base = type_sym[1]
|
|
35
|
+
values = Array(value)
|
|
36
|
+
ptr = TypeMap.allocate_array(base, values.length)
|
|
37
|
+
TypeMap.write_array(ptr, base, values)
|
|
38
|
+
arg_values << ptr.to_i
|
|
39
|
+
when :out_array
|
|
40
|
+
base = type_sym[1]
|
|
41
|
+
count = type_sym[2]
|
|
42
|
+
ptr = TypeMap.allocate_array(base, count)
|
|
43
|
+
# Optional initializer values
|
|
44
|
+
if value
|
|
45
|
+
vals = Array(value)
|
|
46
|
+
raise Error, "Initializer length #{vals.length} does not match out array size #{count}" unless vals.length == count
|
|
47
|
+
TypeMap.write_array(ptr, base, vals)
|
|
48
|
+
end
|
|
49
|
+
out_refs << { index: idx, kind: :out_array, base: base, count: count, ptr: ptr }
|
|
50
|
+
arg_values << ptr.to_i
|
|
51
|
+
else
|
|
52
|
+
raise Error, "Unknown array/output form: #{type_sym.inspect}"
|
|
53
|
+
end
|
|
54
|
+
elsif type_sym == :callback
|
|
55
|
+
spec = value
|
|
56
|
+
unless spec.is_a?(Hash) && spec[:kind] == :callback
|
|
57
|
+
raise Error, 'Invalid callback value; expected func signature and block'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
ret_ty = TypeMap.to_fiddle_type(spec[:ret])
|
|
61
|
+
arg_tys = spec[:args].map { |a| TypeMap.to_fiddle_type(a) }
|
|
62
|
+
# Build Ruby proc from block source, e.g., "{|a,b| a+b}"
|
|
63
|
+
# Evaluate proc in a helper context so DSL methods are available
|
|
64
|
+
ctx = Object.new.extend(Libcall::Fiddley::DSL)
|
|
65
|
+
begin
|
|
66
|
+
ruby_proc = ctx.instance_eval("proc #{spec[:block]}", __FILE__, __LINE__)
|
|
67
|
+
rescue SyntaxError => e
|
|
68
|
+
raise Error, "Invalid Ruby block for callback: #{e.message}"
|
|
69
|
+
end
|
|
70
|
+
closure = Fiddle::Closure::BlockCaller.new(ret_ty, arg_tys) do |*cb_args|
|
|
71
|
+
# Convert pointer-typed args to Fiddle::Pointer for convenience
|
|
72
|
+
cooked = cb_args.each_with_index.map do |v, i|
|
|
73
|
+
at = spec[:args][i]
|
|
74
|
+
if at == :voidp
|
|
75
|
+
Fiddle::Pointer.new(v)
|
|
76
|
+
else
|
|
77
|
+
v
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
ruby_proc.call(*cooked)
|
|
81
|
+
end
|
|
82
|
+
closures << closure # keep alive during call
|
|
83
|
+
arg_values << closure
|
|
30
84
|
else
|
|
31
85
|
arg_values << value
|
|
32
86
|
end
|
|
33
87
|
end
|
|
34
88
|
|
|
35
|
-
ret_type =
|
|
89
|
+
ret_type = TypeMap.to_fiddle_type(return_type)
|
|
36
90
|
|
|
37
91
|
handle = Fiddle.dlopen(lib_path)
|
|
38
92
|
func_ptr = handle[func_name]
|
|
@@ -74,43 +128,20 @@ module Libcall
|
|
|
74
128
|
end
|
|
75
129
|
end
|
|
76
130
|
|
|
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
131
|
def read_output_values(out_refs)
|
|
94
132
|
out_refs.map do |ref|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 }
|
|
133
|
+
case ref[:kind]
|
|
134
|
+
when :out
|
|
135
|
+
value = TypeMap.read_output_pointer(ref[:ptr], ref[:type])
|
|
136
|
+
{ index: ref[:index], type: ref[:type].to_s, value: value }
|
|
137
|
+
when :out_array
|
|
138
|
+
base = ref[:base]
|
|
139
|
+
count = ref[:count]
|
|
140
|
+
values = TypeMap.read_array(ref[:ptr], base, count)
|
|
141
|
+
{ index: ref[:index], type: "#{base}[#{count}]", value: values }
|
|
142
|
+
else
|
|
143
|
+
raise Error, "Unknown out reference kind: #{ref[:kind]}"
|
|
144
|
+
end
|
|
114
145
|
end
|
|
115
146
|
end
|
|
116
147
|
end
|
data/lib/libcall/cli.rb
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require_relative 'platform'
|
|
4
5
|
|
|
5
6
|
module Libcall
|
|
6
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 = {
|
|
@@ -57,19 +76,33 @@ module Libcall
|
|
|
57
76
|
private
|
|
58
77
|
|
|
59
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
|
|
86
|
+
|
|
60
87
|
<<~BANNER
|
|
61
88
|
Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
|
|
62
89
|
|
|
63
90
|
Call C functions in shared libraries from the command line.
|
|
64
91
|
|
|
65
|
-
|
|
92
|
+
Arguments are passed as TYPE VALUE pairs.
|
|
66
93
|
|
|
67
94
|
Examples:
|
|
68
|
-
|
|
69
|
-
libcall -ltest -L ./build add_i32 int 10 int -23 -r int
|
|
70
|
-
libcall --dry-run ./mylib.so test u64 42 -r void
|
|
95
|
+
#{examples.lines.map { |line| line.chomp }.join("\n ")}
|
|
71
96
|
|
|
72
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
|
|
73
106
|
BANNER
|
|
74
107
|
end
|
|
75
108
|
|
|
@@ -122,9 +155,18 @@ module Libcall
|
|
|
122
155
|
|
|
123
156
|
type_sym = Parser.parse_type(type_tok)
|
|
124
157
|
|
|
125
|
-
# TYPE that represents an output pointer does not require a value
|
|
126
|
-
|
|
127
|
-
|
|
158
|
+
# TYPE that represents an output pointer/array does not require a value
|
|
159
|
+
# For out:TYPE[N], allow an optional comma-separated initializer list right after the type.
|
|
160
|
+
if type_sym.is_a?(Array) && %i[out out_array].include?(type_sym.first)
|
|
161
|
+
if type_sym.first == :out_array && i < argv.length && argv[i].include?(',')
|
|
162
|
+
init_tok = argv[i]
|
|
163
|
+
i += 1
|
|
164
|
+
base = type_sym[1]
|
|
165
|
+
values = Parser.coerce_value([:array, base], init_tok)
|
|
166
|
+
arg_pairs << [type_sym, values]
|
|
167
|
+
else
|
|
168
|
+
arg_pairs << [type_sym, nil]
|
|
169
|
+
end
|
|
128
170
|
next
|
|
129
171
|
end
|
|
130
172
|
|
|
@@ -256,18 +298,16 @@ module Libcall
|
|
|
256
298
|
end
|
|
257
299
|
|
|
258
300
|
puts JSON.pretty_generate(output, allow_nan: true)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
puts " [#{out[:index]}] #{out[:type]} = #{out[:value]}"
|
|
266
|
-
end
|
|
301
|
+
elsif result.is_a?(Hash) && result.key?(:outputs)
|
|
302
|
+
puts "Result: #{result[:result]}" unless result[:result].nil?
|
|
303
|
+
unless result[:outputs].empty?
|
|
304
|
+
puts 'Output parameters:'
|
|
305
|
+
result[:outputs].each do |out|
|
|
306
|
+
puts " [#{out[:index]}] #{out[:type]} = #{out[:value]}"
|
|
267
307
|
end
|
|
268
|
-
else
|
|
269
|
-
puts result unless result.nil?
|
|
270
308
|
end
|
|
309
|
+
else
|
|
310
|
+
puts result unless result.nil?
|
|
271
311
|
end
|
|
272
312
|
end
|
|
273
313
|
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A tiny helper layer inspired by fiddley (BSD-2-Clause) to offer
|
|
4
|
+
# convenient, DSL-friendly utilities on top of Ruby's Fiddle.
|
|
5
|
+
# This is intentionally small and tailored for libcall use-cases.
|
|
6
|
+
|
|
7
|
+
require 'fiddle'
|
|
8
|
+
|
|
9
|
+
module Libcall
|
|
10
|
+
module Fiddley
|
|
11
|
+
# Small helper methods intended for use inside callback blocks
|
|
12
|
+
module DSL
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def ptr(x)
|
|
16
|
+
Fiddle::Pointer.new(Integer(x))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def read(type, p)
|
|
20
|
+
t = type.is_a?(Symbol) ? type : type.to_s
|
|
21
|
+
t_sym = Libcall::TypeMap.lookup(t) || (type.is_a?(Symbol) ? type : nil)
|
|
22
|
+
raise Libcall::Error, "unknown read type: #{type}" unless t_sym
|
|
23
|
+
|
|
24
|
+
pp = p.is_a?(Fiddle::Pointer) ? p : Fiddle::Pointer.new(Integer(p))
|
|
25
|
+
Libcall::TypeMap.read_output_pointer(pp, t_sym)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Typed short-hands
|
|
29
|
+
def char(p) = read(:char, p)
|
|
30
|
+
def uchar(p) = read(:uchar, p)
|
|
31
|
+
def short(p) = read(:short, p)
|
|
32
|
+
def ushort(p) = read(:ushort, p)
|
|
33
|
+
def int(p) = read(:int, p)
|
|
34
|
+
def uint(p) = read(:uint, p)
|
|
35
|
+
def long(p) = read(:long, p)
|
|
36
|
+
def ulong(p) = read(:ulong, p)
|
|
37
|
+
def long_long(p) = read(:long_long, p)
|
|
38
|
+
def ulong_long(p) = read(:ulong_long, p)
|
|
39
|
+
def float(p) = read(:float, p)
|
|
40
|
+
def double(p) = read(:double, p)
|
|
41
|
+
def cstr(p) = read(:string, p)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module Utils
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
# Native size_t pack template and size
|
|
48
|
+
SIZET_PACK = (Fiddle::SIZEOF_VOIDP == Fiddle::SIZEOF_LONG ? 'L!' : 'Q')
|
|
49
|
+
|
|
50
|
+
# Return size in bytes for a given type symbol.
|
|
51
|
+
# Falls back to pointer size for :pointer and :voidp.
|
|
52
|
+
def sizeof(type)
|
|
53
|
+
return Fiddle::SIZEOF_SIZE_T if type == :size_t
|
|
54
|
+
return Fiddle::SIZEOF_VOIDP if %i[pointer voidp].include?(type)
|
|
55
|
+
|
|
56
|
+
# Delegate to Libcall::TypeMap when possible
|
|
57
|
+
Libcall::TypeMap.sizeof(type)
|
|
58
|
+
rescue StandardError
|
|
59
|
+
raise Libcall::Error, "unknown type for sizeof: #{type}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Convert a type symbol to a Fiddle type constant.
|
|
63
|
+
def to_fiddle_type(type)
|
|
64
|
+
return Fiddle::TYPE_SIZE_T if type == :size_t
|
|
65
|
+
return Fiddle::TYPE_VOIDP if %i[pointer voidp].include?(type)
|
|
66
|
+
|
|
67
|
+
Libcall::TypeMap.to_fiddle_type(type)
|
|
68
|
+
rescue StandardError
|
|
69
|
+
raise Libcall::Error, "unknown type for to_fiddle_type: #{type}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Pack template for array values of given base type.
|
|
73
|
+
def array_pack_template(type)
|
|
74
|
+
return SIZET_PACK if type == :size_t
|
|
75
|
+
|
|
76
|
+
# Use TypeMap's packing for standard types
|
|
77
|
+
Libcall::TypeMap.pack_template(type)
|
|
78
|
+
rescue StandardError
|
|
79
|
+
# For generic pointers/addresses, use native unsigned pointer width
|
|
80
|
+
return 'J' if %i[pointer voidp].include?(type)
|
|
81
|
+
|
|
82
|
+
raise Libcall::Error, "Unsupported array base type: #{type}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Convert Ruby array of numbers to a binary string for the given type
|
|
86
|
+
def array2str(type, array)
|
|
87
|
+
array.pack("#{array_pack_template(type)}*")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Convert binary string to Ruby array of the given type
|
|
91
|
+
def str2array(type, str)
|
|
92
|
+
str.unpack("#{array_pack_template(type)}*")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Minimal memory buffer wrapper to make building args/arrays convenient
|
|
97
|
+
class MemoryPointer
|
|
98
|
+
attr_reader :size
|
|
99
|
+
|
|
100
|
+
def initialize(type, count = 1)
|
|
101
|
+
@type = type
|
|
102
|
+
@size = Utils.sizeof(type) * count
|
|
103
|
+
@ptr = Fiddle::Pointer.malloc(@size)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_ptr
|
|
107
|
+
@ptr
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def address
|
|
111
|
+
@ptr.to_i
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def write_array(type, values)
|
|
115
|
+
data = Utils.array2str(type, Array(values))
|
|
116
|
+
@ptr[0, data.bytesize] = data
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def read_array(type, count)
|
|
121
|
+
bytes = Utils.sizeof(type) * count
|
|
122
|
+
Utils.str2array(type, @ptr[0, bytes])
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def put_bytes(offset, str)
|
|
126
|
+
@ptr[offset, str.bytesize] = str
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def write_bytes(str)
|
|
130
|
+
put_bytes(0, str)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def get_bytes(offset, len)
|
|
134
|
+
@ptr[offset, len]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def read_bytes(len)
|
|
138
|
+
get_bytes(0, len)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Return Fiddle::Pointer stored at this pointer (read void*)
|
|
142
|
+
def read_pointer
|
|
143
|
+
to_ptr.ptr
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Wrap Fiddle::Closure::BlockCaller with friendlier type mapping
|
|
148
|
+
class Function < Fiddle::Closure::BlockCaller
|
|
149
|
+
def initialize(ret, params, &blk)
|
|
150
|
+
r = Utils.to_fiddle_type(ret)
|
|
151
|
+
p = Array(params).map { |t| Utils.to_fiddle_type(t) }
|
|
152
|
+
super(r, p, &blk)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
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'
|
|
@@ -19,29 +20,23 @@ module Libcall
|
|
|
19
20
|
# Find library by name (e.g., "m" -> "/lib/x86_64-linux-gnu/libm.so.6")
|
|
20
21
|
def find(lib_name)
|
|
21
22
|
# If it's a path, return as-is
|
|
22
|
-
return File.expand_path(lib_name) if
|
|
23
|
-
return File.expand_path(lib_name) if File.file?(lib_name)
|
|
23
|
+
return File.expand_path(lib_name) if path_like?(lib_name)
|
|
24
24
|
|
|
25
25
|
search_paths = @lib_paths + @default_paths
|
|
26
26
|
|
|
27
27
|
if defined?(PKGConfig)
|
|
28
|
-
pkg_exists =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
33
36
|
|
|
34
37
|
if pkg_exists
|
|
35
|
-
lib_dirs =
|
|
36
|
-
|
|
37
|
-
else
|
|
38
|
-
PKGConfig.libs(lib_name).to_s.split.select { |t| t.start_with?('-L') }.map { |t| t[2..] }
|
|
39
|
-
end
|
|
40
|
-
lib_names = if PKGConfig.respond_to?(:libs_only_l)
|
|
41
|
-
PKGConfig.libs_only_l(lib_name).to_s.split.map { |l| l.start_with?('-l') ? l[2..] : l }
|
|
42
|
-
else
|
|
43
|
-
PKGConfig.libs(lib_name).to_s.split.select { |t| t.start_with?('-l') }.map { |t| t[2..] }
|
|
44
|
-
end
|
|
38
|
+
lib_dirs = extract_pkg_config_flags(lib_name, 'L')
|
|
39
|
+
lib_names = extract_pkg_config_flags(lib_name, 'l')
|
|
45
40
|
|
|
46
41
|
search_paths = lib_dirs + search_paths
|
|
47
42
|
|
|
@@ -62,53 +57,65 @@ module Libcall
|
|
|
62
57
|
|
|
63
58
|
private
|
|
64
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
|
+
|
|
65
79
|
def default_library_paths
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
(Platform.windows? ? windows_library_paths : unix_library_paths)
|
|
81
|
+
.select { |p| Dir.exist?(p) }
|
|
82
|
+
end
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
else
|
|
83
|
-
# Unix-like systems (Linux, macOS)
|
|
84
|
-
# Standard library paths
|
|
85
|
-
paths << '/lib'
|
|
86
|
-
paths << '/usr/lib'
|
|
87
|
-
paths << '/usr/local/lib'
|
|
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
|
|
84
|
+
def windows_library_paths
|
|
85
|
+
paths = %w[C:/Windows/System32 C:/Windows/SysWOW64]
|
|
97
86
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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"])
|
|
91
|
+
end
|
|
103
92
|
|
|
104
|
-
|
|
105
|
-
|
|
93
|
+
# Add PATH directories on Windows
|
|
94
|
+
paths.concat(ENV['PATH'].split(';').map { |p| p.tr('\\', '/') }) if ENV['PATH']
|
|
106
95
|
|
|
107
|
-
|
|
108
|
-
|
|
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])
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
# macOS paths
|
|
112
|
+
paths.concat(%w[/usr/local/lib /opt/homebrew/lib]) if Platform.darwin?
|
|
113
|
+
|
|
114
|
+
# Environment-based paths
|
|
115
|
+
paths.concat(ENV.fetch('LD_LIBRARY_PATH', '').split(':'))
|
|
116
|
+
paths.concat(ENV.fetch('DYLD_LIBRARY_PATH', '').split(':'))
|
|
117
|
+
|
|
118
|
+
paths
|
|
112
119
|
end
|
|
113
120
|
|
|
114
121
|
def resolve_by_name_in_paths(lib_name, search_paths)
|
|
@@ -119,31 +126,19 @@ module Libcall
|
|
|
119
126
|
end
|
|
120
127
|
|
|
121
128
|
# Try with lib prefix and common extensions
|
|
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
129
|
prefixes = lib_name.start_with?('lib') ? [''] : ['lib', '']
|
|
130
|
+
extensions = Platform.library_extensions
|
|
131
131
|
|
|
132
|
-
prefixes.each do |prefix|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
full_path = File.join(path, name)
|
|
137
|
-
return File.expand_path(full_path) if File.file?(full_path)
|
|
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)
|
|
138
136
|
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
# Check for versioned libraries (libm.so.6, etc.)
|
|
138
|
+
next if ext.empty?
|
|
141
139
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return File.expand_path(matches.first) unless matches.empty?
|
|
145
|
-
end
|
|
146
|
-
end
|
|
140
|
+
matches = Dir.glob(File.join(path, "#{name}.*")).select { |f| File.file?(f) }
|
|
141
|
+
return File.expand_path(matches.first) unless matches.empty?
|
|
147
142
|
end
|
|
148
143
|
|
|
149
144
|
nil
|
data/lib/libcall/parser.rb
CHANGED
|
@@ -1,51 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'type_map'
|
|
4
|
+
|
|
3
5
|
module Libcall
|
|
4
6
|
# Parse and coerce TYPE VALUE argument pairs for FFI calls
|
|
5
7
|
class Parser
|
|
6
|
-
TYPE_MAP = {
|
|
7
|
-
'i8' => :char,
|
|
8
|
-
'u8' => :uchar,
|
|
9
|
-
'i16' => :short,
|
|
10
|
-
'u16' => :ushort,
|
|
11
|
-
'i32' => :int,
|
|
12
|
-
'u32' => :uint,
|
|
13
|
-
'i64' => :long_long,
|
|
14
|
-
'u64' => :ulong_long,
|
|
15
|
-
'isize' => :long,
|
|
16
|
-
'usize' => :ulong,
|
|
17
|
-
'f32' => :float,
|
|
18
|
-
'f64' => :double,
|
|
19
|
-
'cstr' => :string,
|
|
20
|
-
'ptr' => :voidp,
|
|
21
|
-
'pointer' => :voidp,
|
|
22
|
-
'void' => :void,
|
|
23
|
-
# Common aliases
|
|
24
|
-
'int' => :int,
|
|
25
|
-
'uint' => :uint,
|
|
26
|
-
'long' => :long,
|
|
27
|
-
'ulong' => :ulong,
|
|
28
|
-
'float' => :float,
|
|
29
|
-
'double' => :double,
|
|
30
|
-
'char' => :char,
|
|
31
|
-
'str' => :string,
|
|
32
|
-
'string' => :string
|
|
33
|
-
}.freeze
|
|
34
|
-
|
|
35
|
-
INTEGER_TYPES = %i[int uint long ulong long_long ulong_long char uchar short ushort].freeze
|
|
36
|
-
FLOAT_TYPES = %i[float double].freeze
|
|
37
|
-
|
|
38
8
|
# Pair-only API helpers
|
|
39
9
|
def self.parse_type(type_str)
|
|
10
|
+
# Callback function pointer: func/callback 'ret(arg,...) { |...| ... }'
|
|
11
|
+
return :callback if %w[func callback].include?(type_str)
|
|
12
|
+
|
|
13
|
+
# Output array spec: out:TYPE[N]
|
|
14
|
+
if type_str.start_with?('out:') && type_str.match(/^out:(.+)\[(\d+)\]$/)
|
|
15
|
+
base = Regexp.last_match(1)
|
|
16
|
+
count = Regexp.last_match(2).to_i
|
|
17
|
+
base_sym = TypeMap.lookup(base)
|
|
18
|
+
raise Error, "Unknown output array type: #{base}" unless base_sym
|
|
19
|
+
|
|
20
|
+
return [:out_array, base_sym, count]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Input array spec: TYPE[] (value as comma-separated list)
|
|
24
|
+
if type_str.end_with?('[]')
|
|
25
|
+
base = type_str[0..-3]
|
|
26
|
+
base_sym = TypeMap.lookup(base)
|
|
27
|
+
raise Error, "Unknown array base type: #{base}" unless base_sym
|
|
28
|
+
|
|
29
|
+
return [:array, base_sym]
|
|
30
|
+
end
|
|
31
|
+
|
|
40
32
|
# Output pointer spec: out:TYPE (e.g., out:int, out:f64)
|
|
41
33
|
if type_str.start_with?('out:')
|
|
42
34
|
inner = type_str.sub(/^out:/, '')
|
|
43
|
-
inner_sym =
|
|
35
|
+
inner_sym = TypeMap.lookup(inner)
|
|
44
36
|
raise Error, "Unknown type in out: #{inner}" unless inner_sym
|
|
37
|
+
|
|
45
38
|
return [:out, inner_sym]
|
|
46
39
|
end
|
|
47
40
|
|
|
48
|
-
type_sym =
|
|
41
|
+
type_sym = TypeMap.lookup(type_str)
|
|
49
42
|
raise Error, "Unknown type: #{type_str}" unless type_sym
|
|
50
43
|
|
|
51
44
|
type_sym
|
|
@@ -54,17 +47,51 @@ module Libcall
|
|
|
54
47
|
def self.parse_return_type(type_str)
|
|
55
48
|
return :void if type_str.nil? || type_str.empty? || type_str == 'void'
|
|
56
49
|
|
|
57
|
-
type_sym =
|
|
50
|
+
type_sym = TypeMap.lookup(type_str)
|
|
58
51
|
raise Error, "Unknown return type: #{type_str}" unless type_sym
|
|
59
52
|
|
|
60
53
|
type_sym
|
|
61
54
|
end
|
|
62
55
|
|
|
63
56
|
def self.coerce_value(type_sym, token)
|
|
57
|
+
# Callback value: signature + Ruby block
|
|
58
|
+
if type_sym == :callback
|
|
59
|
+
src = strip_quotes(token.to_s)
|
|
60
|
+
m = src.match(/\A\s*([^(\s]+)\s*\(([^)]*)\)\s*(\{.*\})\s*\z/m)
|
|
61
|
+
raise Error, "Invalid callback spec: #{src}" unless m
|
|
62
|
+
|
|
63
|
+
ret_s = m[1].strip
|
|
64
|
+
args_s = m[2].strip
|
|
65
|
+
block_src = m[3]
|
|
66
|
+
|
|
67
|
+
ret_sym = TypeMap.lookup(ret_s)
|
|
68
|
+
raise Error, "Unknown callback return type: #{ret_s}" unless ret_sym
|
|
69
|
+
|
|
70
|
+
arg_syms = if args_s.empty?
|
|
71
|
+
[]
|
|
72
|
+
else
|
|
73
|
+
args_s.split(',').map(&:strip).map do |a|
|
|
74
|
+
sym = TypeMap.lookup(a)
|
|
75
|
+
raise Error, "Unknown callback arg type: #{a}" unless sym
|
|
76
|
+
|
|
77
|
+
sym
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
return { kind: :callback, ret: ret_sym, args: arg_syms, block: block_src }
|
|
82
|
+
end
|
|
83
|
+
# Input array values: comma-separated
|
|
84
|
+
if type_sym.is_a?(Array) && type_sym.first == :array
|
|
85
|
+
base = type_sym[1]
|
|
86
|
+
return [] if token.nil? || token.empty?
|
|
87
|
+
|
|
88
|
+
return token.split(',').map { |t| coerce_single_value(base, t.strip) }
|
|
89
|
+
end
|
|
90
|
+
|
|
64
91
|
case type_sym
|
|
65
|
-
when *FLOAT_TYPES
|
|
92
|
+
when *TypeMap::FLOAT_TYPES
|
|
66
93
|
Float(token)
|
|
67
|
-
when *INTEGER_TYPES
|
|
94
|
+
when *TypeMap::INTEGER_TYPES
|
|
68
95
|
Integer(token)
|
|
69
96
|
when :voidp
|
|
70
97
|
# Accept common null tokens for pointer types
|
|
@@ -80,32 +107,24 @@ module Libcall
|
|
|
80
107
|
end
|
|
81
108
|
end
|
|
82
109
|
|
|
83
|
-
def self.
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
def self.coerce_single_value(type_sym, token)
|
|
111
|
+
case type_sym
|
|
112
|
+
when *TypeMap::FLOAT_TYPES
|
|
113
|
+
Float(token)
|
|
114
|
+
when *TypeMap::INTEGER_TYPES
|
|
115
|
+
Integer(token)
|
|
116
|
+
when :string
|
|
117
|
+
strip_quotes(token)
|
|
86
118
|
else
|
|
87
|
-
|
|
119
|
+
raise Error, "Unknown element type for coercion: #{type_sym}"
|
|
88
120
|
end
|
|
89
121
|
end
|
|
90
122
|
|
|
91
|
-
def self.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
case type_sym
|
|
96
|
-
when :void then Fiddle::TYPE_VOID
|
|
97
|
-
when :char then Fiddle::TYPE_CHAR
|
|
98
|
-
when :uchar then Fiddle::TYPE_UCHAR
|
|
99
|
-
when :short then Fiddle::TYPE_SHORT
|
|
100
|
-
when :ushort then Fiddle::TYPE_USHORT
|
|
101
|
-
when :int, :uint then Fiddle::TYPE_INT
|
|
102
|
-
when :long, :ulong then Fiddle::TYPE_LONG
|
|
103
|
-
when :long_long, :ulong_long then Fiddle::TYPE_LONG_LONG
|
|
104
|
-
when :float then Fiddle::TYPE_FLOAT
|
|
105
|
-
when :double then Fiddle::TYPE_DOUBLE
|
|
106
|
-
when :voidp, :string then Fiddle::TYPE_VOIDP
|
|
123
|
+
def self.strip_quotes(token)
|
|
124
|
+
if (token.start_with?('"') && token.end_with?('"')) || (token.start_with?("'") && token.end_with?("'"))
|
|
125
|
+
token[1...-1]
|
|
107
126
|
else
|
|
108
|
-
|
|
127
|
+
token
|
|
109
128
|
end
|
|
110
129
|
end
|
|
111
130
|
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,242 @@
|
|
|
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
|
+
'uchar' => :uchar,
|
|
31
|
+
'short' => :short,
|
|
32
|
+
'ushort' => :ushort,
|
|
33
|
+
'int' => :int,
|
|
34
|
+
'uint' => :uint,
|
|
35
|
+
'long' => :long,
|
|
36
|
+
'ulong' => :ulong,
|
|
37
|
+
'float' => :float,
|
|
38
|
+
'double' => :double,
|
|
39
|
+
# C-style pointer aliases
|
|
40
|
+
'void*' => :voidp,
|
|
41
|
+
'const void*' => :voidp,
|
|
42
|
+
'const_void*' => :voidp,
|
|
43
|
+
'const_voidp' => :voidp,
|
|
44
|
+
# Underscored variants
|
|
45
|
+
'unsigned_char' => :uchar,
|
|
46
|
+
'unsigned_short' => :ushort,
|
|
47
|
+
'unsigned_int' => :uint,
|
|
48
|
+
'unsigned_long' => :ulong,
|
|
49
|
+
'long_long' => :long_long,
|
|
50
|
+
'unsigned_long_long' => :ulong_long,
|
|
51
|
+
# Short aliases
|
|
52
|
+
'unsigned' => :uint,
|
|
53
|
+
'signed' => :int,
|
|
54
|
+
# Extended type names (stdint-like)
|
|
55
|
+
'int8' => :char,
|
|
56
|
+
'uint8' => :uchar,
|
|
57
|
+
'int16' => :short,
|
|
58
|
+
'uint16' => :ushort,
|
|
59
|
+
'int32' => :int,
|
|
60
|
+
'uint32' => :uint,
|
|
61
|
+
'int64' => :long_long,
|
|
62
|
+
'uint64' => :ulong_long,
|
|
63
|
+
'float32' => :float,
|
|
64
|
+
'float64' => :double,
|
|
65
|
+
# C99/C11 standard types with _t suffix
|
|
66
|
+
'int8_t' => :char,
|
|
67
|
+
'uint8_t' => :uchar,
|
|
68
|
+
'int16_t' => :short,
|
|
69
|
+
'uint16_t' => :ushort,
|
|
70
|
+
'int32_t' => :int,
|
|
71
|
+
'uint32_t' => :uint,
|
|
72
|
+
'int64_t' => :long_long,
|
|
73
|
+
'uint64_t' => :ulong_long,
|
|
74
|
+
# Size and pointer-sized integers
|
|
75
|
+
'size_t' => :ulong,
|
|
76
|
+
'ssize_t' => :long,
|
|
77
|
+
'intptr' => :long,
|
|
78
|
+
'uintptr' => :ulong,
|
|
79
|
+
'intptr_t' => :long,
|
|
80
|
+
'uintptr_t' => :ulong,
|
|
81
|
+
'ptrdiff_t' => :long,
|
|
82
|
+
# Boolean
|
|
83
|
+
'bool' => :int,
|
|
84
|
+
# String aliases
|
|
85
|
+
'str' => :string,
|
|
86
|
+
'string' => :string
|
|
87
|
+
}.freeze
|
|
88
|
+
|
|
89
|
+
# Integer type symbols
|
|
90
|
+
INTEGER_TYPES = %i[
|
|
91
|
+
int uint
|
|
92
|
+
long ulong
|
|
93
|
+
long_long ulong_long
|
|
94
|
+
char uchar
|
|
95
|
+
short ushort
|
|
96
|
+
].freeze
|
|
97
|
+
|
|
98
|
+
# Floating point type symbols
|
|
99
|
+
FLOAT_TYPES = %i[float double].freeze
|
|
100
|
+
|
|
101
|
+
# Look up FFI type symbol from string
|
|
102
|
+
def self.lookup(type_str)
|
|
103
|
+
MAP[type_str]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Check if type symbol is an integer type
|
|
107
|
+
def self.integer_type?(type_sym)
|
|
108
|
+
INTEGER_TYPES.include?(type_sym)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Check if type symbol is a floating point type
|
|
112
|
+
def self.float_type?(type_sym)
|
|
113
|
+
FLOAT_TYPES.include?(type_sym)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Convert type symbol to Fiddle type constant
|
|
117
|
+
def self.to_fiddle_type(type_sym)
|
|
118
|
+
# Array and output parameters are passed as pointers
|
|
119
|
+
if type_sym.is_a?(Array)
|
|
120
|
+
tag = type_sym.first
|
|
121
|
+
return Fiddle::TYPE_VOIDP if %i[out array out_array].include?(tag)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Callback function pointers are passed as void*
|
|
125
|
+
return Fiddle::TYPE_VOIDP if type_sym == :callback
|
|
126
|
+
|
|
127
|
+
case type_sym
|
|
128
|
+
when :void then Fiddle::TYPE_VOID
|
|
129
|
+
when :char then Fiddle::TYPE_CHAR
|
|
130
|
+
when :uchar then Fiddle::TYPE_UCHAR
|
|
131
|
+
when :short then Fiddle::TYPE_SHORT
|
|
132
|
+
when :ushort then Fiddle::TYPE_USHORT
|
|
133
|
+
when :int, :uint then Fiddle::TYPE_INT
|
|
134
|
+
when :long, :ulong then Fiddle::TYPE_LONG
|
|
135
|
+
when :long_long, :ulong_long then Fiddle::TYPE_LONG_LONG
|
|
136
|
+
when :float then Fiddle::TYPE_FLOAT
|
|
137
|
+
when :double then Fiddle::TYPE_DOUBLE
|
|
138
|
+
when :voidp, :string then Fiddle::TYPE_VOIDP
|
|
139
|
+
else
|
|
140
|
+
raise Error, "Unknown Fiddle type: #{type_sym}"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get the size in bytes for a type symbol
|
|
145
|
+
def self.sizeof(type_sym)
|
|
146
|
+
case type_sym
|
|
147
|
+
when :char, :uchar then Fiddle::SIZEOF_CHAR
|
|
148
|
+
when :short, :ushort then Fiddle::SIZEOF_SHORT
|
|
149
|
+
when :int, :uint then Fiddle::SIZEOF_INT
|
|
150
|
+
when :long, :ulong then Fiddle::SIZEOF_LONG
|
|
151
|
+
when :long_long, :ulong_long then Fiddle::SIZEOF_LONG_LONG
|
|
152
|
+
when :float then Fiddle::SIZEOF_FLOAT
|
|
153
|
+
when :double then Fiddle::SIZEOF_DOUBLE
|
|
154
|
+
when :voidp, :string then Fiddle::SIZEOF_VOIDP
|
|
155
|
+
else
|
|
156
|
+
raise Error, "Cannot get size for type: #{type_sym}"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Allocate a pointer for output parameter
|
|
161
|
+
def self.allocate_output_pointer(type_sym)
|
|
162
|
+
ptr = Fiddle::Pointer.malloc(sizeof(type_sym))
|
|
163
|
+
# For out:string, we pass char**. Initialize inner pointer to NULL for safety.
|
|
164
|
+
ptr[0, Fiddle::SIZEOF_VOIDP] = [0].pack('J') if type_sym == :string
|
|
165
|
+
ptr
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Read value from output pointer
|
|
169
|
+
def self.read_output_pointer(ptr, type_sym)
|
|
170
|
+
case type_sym
|
|
171
|
+
when :char then ptr[0, Fiddle::SIZEOF_CHAR].unpack1('c')
|
|
172
|
+
when :uchar then ptr[0, Fiddle::SIZEOF_CHAR].unpack1('C')
|
|
173
|
+
when :short then ptr[0, Fiddle::SIZEOF_SHORT].unpack1('s')
|
|
174
|
+
when :ushort then ptr[0, Fiddle::SIZEOF_SHORT].unpack1('S')
|
|
175
|
+
when :int then ptr[0, Fiddle::SIZEOF_INT].unpack1('i')
|
|
176
|
+
when :uint then ptr[0, Fiddle::SIZEOF_INT].unpack1('I')
|
|
177
|
+
when :long then ptr[0, Fiddle::SIZEOF_LONG].unpack1('l!')
|
|
178
|
+
when :ulong then ptr[0, Fiddle::SIZEOF_LONG].unpack1('L!')
|
|
179
|
+
when :long_long then ptr[0, Fiddle::SIZEOF_LONG_LONG].unpack1('q')
|
|
180
|
+
when :ulong_long then ptr[0, Fiddle::SIZEOF_LONG_LONG].unpack1('Q')
|
|
181
|
+
when :float then ptr[0, Fiddle::SIZEOF_FLOAT].unpack1('f')
|
|
182
|
+
when :double then ptr[0, Fiddle::SIZEOF_DOUBLE].unpack1('d')
|
|
183
|
+
when :string
|
|
184
|
+
addr = ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J')
|
|
185
|
+
return '(null)' if addr.zero?
|
|
186
|
+
|
|
187
|
+
begin
|
|
188
|
+
Fiddle::Pointer.new(addr).to_s
|
|
189
|
+
rescue StandardError
|
|
190
|
+
format('0x%x', addr)
|
|
191
|
+
end
|
|
192
|
+
when :voidp then format('0x%x', ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J'))
|
|
193
|
+
else
|
|
194
|
+
raise Error, "Cannot read output value for type: #{type_sym}"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Read a single scalar value at address (helper for callbacks)
|
|
199
|
+
def self.read_scalar(ptr, type_sym)
|
|
200
|
+
read_output_pointer(ptr, type_sym)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Allocate memory for an array of base type and count elements
|
|
204
|
+
def self.allocate_array(base_type, count)
|
|
205
|
+
Fiddle::Pointer.malloc(sizeof(base_type) * count)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def self.write_array(ptr, base_type, values)
|
|
209
|
+
return if values.nil? || values.empty?
|
|
210
|
+
|
|
211
|
+
bytes = sizeof(base_type) * values.length
|
|
212
|
+
ptr[0, bytes] = values.pack(pack_template(base_type) + values.length.to_s)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def self.read_array(ptr, base_type, count)
|
|
216
|
+
return [] if count <= 0
|
|
217
|
+
|
|
218
|
+
bytes = sizeof(base_type) * count
|
|
219
|
+
raw = ptr[0, bytes]
|
|
220
|
+
raw.unpack(pack_template(base_type) + count.to_s)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def self.pack_template(base_type)
|
|
224
|
+
case base_type
|
|
225
|
+
when :char then 'c'
|
|
226
|
+
when :uchar then 'C'
|
|
227
|
+
when :short then 's'
|
|
228
|
+
when :ushort then 'S'
|
|
229
|
+
when :int then 'i'
|
|
230
|
+
when :uint then 'I'
|
|
231
|
+
when :long then 'l!'
|
|
232
|
+
when :ulong then 'L!'
|
|
233
|
+
when :long_long then 'q'
|
|
234
|
+
when :ulong_long then 'Q'
|
|
235
|
+
when :float then 'f'
|
|
236
|
+
when :double then 'd'
|
|
237
|
+
else
|
|
238
|
+
raise Error, "Unsupported array base type: #{base_type}"
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
data/lib/libcall/version.rb
CHANGED
data/lib/libcall.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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kojix2
|
|
@@ -49,8 +49,11 @@ files:
|
|
|
49
49
|
- lib/libcall.rb
|
|
50
50
|
- lib/libcall/caller.rb
|
|
51
51
|
- lib/libcall/cli.rb
|
|
52
|
+
- lib/libcall/fiddley.rb
|
|
52
53
|
- lib/libcall/library_finder.rb
|
|
53
54
|
- lib/libcall/parser.rb
|
|
55
|
+
- lib/libcall/platform.rb
|
|
56
|
+
- lib/libcall/type_map.rb
|
|
54
57
|
- lib/libcall/version.rb
|
|
55
58
|
homepage: https://github.com/kojix2/libcall
|
|
56
59
|
licenses:
|
|
@@ -70,7 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
70
73
|
- !ruby/object:Gem::Version
|
|
71
74
|
version: '0'
|
|
72
75
|
requirements: []
|
|
73
|
-
rubygems_version: 3.
|
|
76
|
+
rubygems_version: 3.7.2
|
|
74
77
|
specification_version: 4
|
|
75
78
|
summary: Call functions in shared libraries directly from the CLI
|
|
76
79
|
test_files: []
|