libcall 0.0.2 → 0.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b45f825f6989bd4673b982435ce1bad8ff1b1785785118ffa66c8bdad84b1d7c
4
- data.tar.gz: 9a60c1d6d69b2ff7e70a2884b6dfbc8e2b6099c87df40558559fdffffff5c65c
3
+ metadata.gz: 6d3e1c356b60aca8356cd319a9c31619850cb4cbee3275d94f04bc0f3d18b422
4
+ data.tar.gz: 75ebf8ecc71c6d38f34c4b1f1b6be809e95cc2d5f24cba21755cbf3455ccd9fb
5
5
  SHA512:
6
- metadata.gz: ed0cde569e1eea83480b744907d3397f6e7abae68b991993090312edc6ccf74d4cf973afbad2dd3402142eccf390a3f992cf2174ad016ea0009d5d8b91e104bd
7
- data.tar.gz: 42571c69fd4e1487e17b592ec98afc321ed646e938d9d773a96c4e450f4762c9d49975f7de47a33ba5f747e2e2c1367da00d7a3233ff059a8ab5a70f43a3bb92
6
+ metadata.gz: fcebbe2dd033a7392ee4bc5b5efed51e5352f6cc1a1dee9aa33751e9833703a6574853265698851548c1b1cda17fad437f450f6058b2a664b7b599c049b44e48
7
+ data.tar.gz: f3f36070e3aec368a843d72851c88bc25e86a8f04bc23e4950e92e0eda46a23a8980d25598c697069c093f3083b0476d2f6bd1d082ec2dc46e64dd1af661fe6e
data/README.md CHANGED
@@ -12,50 +12,20 @@ Call C functions in shared libraries from the command line.
12
12
  gem install libcall
13
13
  ```
14
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
-
17
- ## Usage
18
-
19
- ```sh
20
- libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
21
- ```
22
-
23
15
  ### Quick Examples
24
16
 
25
17
  ```sh
26
- # TYPE VALUE pairs
27
- libcall -lm -r f64 sqrt double 16
28
- # => 4.0
29
-
30
- # libc strlen
31
- libcall -lc strlen string "hello" -r usize
32
- # => 5
18
+ libcall -lm sqrt double 16 -r double # => 4.0
33
19
  ```
34
20
 
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
21
  ```sh
48
- # Pass a null pointer to a function taking const char*
49
- libcall -ltest str_length ptr null -r i32
50
- # => 0
22
+ libcall -lc strlen string "hello" -r usize # => 5
51
23
  ```
52
24
 
53
- End of options `--`:
54
-
55
- - Use `--` to stop option parsing if a value starts with `-`
25
+ ## Usage
56
26
 
57
27
  ```sh
58
- libcall -lc getenv string -- -r -r cstr
28
+ libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
59
29
  ```
60
30
 
61
31
  ### Options
@@ -76,48 +46,119 @@ Library search:
76
46
 
77
47
  ### More Examples
78
48
 
49
+ Output parameter with libm
50
+
51
+ ```sh
52
+ libcall -lm modf double -3.14 out:double -r f64
53
+ # Result: -0.14000000000000012
54
+ # Output parameters:
55
+ # [1] double = -3.0
56
+ ```
57
+
58
+ JSON output
59
+
79
60
  ```sh
80
- # JSON output
81
61
  libcall --json -lm sqrt double 9.0 -r f64
62
+ # {
63
+ # "library": "/lib/x86_64-linux-gnu/libm.so",
64
+ # "function": "sqrt",
65
+ # "return_type": "double",
66
+ # "result": 3.0
67
+ # }
68
+ ```
69
+
70
+ Dry run
82
71
 
83
- # Dry run
72
+ ```sh
84
73
  libcall --dry-run -lc getpid -r int
74
+ # Library: /lib/x86_64-linux-gnu/libc.so
75
+ # Function: getpid
76
+ # Return: int
77
+ ```
85
78
 
86
- # Output parameter with libm
87
- libcall -lm modf double -3.14 out:double -r f64
79
+ ## Type Reference
88
80
 
89
- # TYPE/VALUE pairs with -r after function
90
- libcall -lm fabs double -5.5 -r f64
91
- # => 5.5
81
+ libcall supports multiple naming conventions for types, making it easy to work with C libraries.
92
82
 
93
- # Windows: calling C runtime functions
94
- libcall msvcrt.dll sqrt double 16.0 -r f64
95
- # => 4.0
83
+ ### Integer Types
96
84
 
97
- # Windows: accessing environment variables
98
- libcall msvcrt.dll getenv string "PATH" -r cstr
85
+ | Short (Rust-like) | C Standard | C99/stdint.h | Size |
86
+ | ----------------- | -------------------------- | ---------------------- | ------------------ |
87
+ | `i8` / `u8` | `char` / `uchar` | `int8_t` / `uint8_t` | 1 byte |
88
+ | `i16` / `u16` | `short` / `ushort` | `int16_t` / `uint16_t` | 2 bytes |
89
+ | `i32` / `u32` | `int` / `uint` | `int32_t` / `uint32_t` | 4 bytes |
90
+ | `i64` / `u64` | `long_long` / `ulong_long` | `int64_t` / `uint64_t` | 8 bytes |
91
+ | `isize` / `usize` | `long` / `ulong` | `ssize_t` / `size_t` | platform-dependent |
92
+
93
+ **Alternative names**: You can use any of these:
94
+
95
+ - C-style: `char`, `short`, `int`, `long`, `unsigned_int`, etc.
96
+ - stdint-style: `int8`, `int16`, `int32`, `int64`, `uint8`, `uint16`, etc.
97
+ - With `_t` suffix: `int8_t`, `uint8_t`, `int32_t`, `size_t`, etc.
98
+
99
+ ### Floating Point Types
100
+
101
+ | Short | C Standard | Alternative | Size |
102
+ | ----- | ---------- | ----------- | ------- |
103
+ | `f32` | `float` | `float32` | 4 bytes |
104
+ | `f64` | `double` | `float64` | 8 bytes |
105
+
106
+ ### Pointer Types
107
+
108
+ | Type | Description | Usage |
109
+ | --------------------------- | ------------------------ | ------------------------------ |
110
+ | `ptr` / `pointer` / `voidp` | Generic pointer (void\*) | For arbitrary memory addresses |
111
+ | `string` / `cstr` / `str` | C string (char\*) | For passing/returning strings |
112
+
113
+ **Null pointer values**: Use `null`, `NULL`, `nil`, or `0` to pass a null pointer.
114
+
115
+ ### Special Types
116
+
117
+ | Type | Description | Alternative names |
118
+ | ------------------------ | --------------------------- | -------------------- |
119
+ | `void` | No value (return type only) | — |
120
+ | `size_t` | Platform size type | `usize` (unsigned) |
121
+ | `ssize_t` | Signed size type | `isize` (signed) |
122
+ | `intptr_t` / `uintptr_t` | Pointer-sized integer | `intptr` / `uintptr` |
123
+ | `ptrdiff_t` | Pointer difference type | — |
124
+ | `bool` | Boolean (as int) | — |
125
+
126
+ ### Output Parameters
127
+
128
+ Prefix any type with `out:` to create an output parameter:
129
+
130
+ ```sh
131
+ out:int # Output integer pointer (int*)
132
+ out:double # Output double pointer (double*)
133
+ out:string # Output string pointer (char**)
99
134
  ```
100
135
 
101
- ## Type Reference
136
+ ### Array Types
137
+
138
+ | Syntax | Description | Example |
139
+ | ------------- | ----------------------------- | -------------------- |
140
+ | `TYPE[]` | Input array | `int[] 1,2,3,4,5` |
141
+ | `out:TYPE[N]` | Output array of N elements | `out:int[10]` |
142
+ | `out:TYPE[N]` | Output array with initializer | `out:int[4] 4,3,2,1` |
102
143
 
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
144
+ ### Callback Types
145
+
146
+ | Keyword | Description | Example |
147
+ | ---------- | --------------------------- | -------------------------------- |
148
+ | `func` | Function pointer (callback) | `func 'int(int a,int b){ a+b }'` |
149
+ | `callback` | Alias for `func` | Same as above |
150
+
151
+ ### Argument Syntax
152
+
153
+ Pass arguments as TYPE VALUE pairs:
154
+
155
+ ```sh
156
+ libcall -lm sqrt double 16.0 -r f64
157
+ libcall -lc strlen string "hello" -r usize
158
+ ```
159
+
160
+ - Null pointers: Use `null`, `NULL`, `nil`, or `0`
161
+ - Negative numbers work as expected (e.g., `double -3.14`)
121
162
 
122
163
  ## pkg-config Support
123
164
 
@@ -160,6 +201,42 @@ libcall -lc getrandom out:uchar[16] size_t 16 uint 0 -r long
160
201
  libcall -lSystem arc4random_buf out:uchar[16] size_t 16 -r void
161
202
  ```
162
203
 
204
+ ## Callbacks (experimental)
205
+
206
+ Pass a C function pointer via a Ruby callback. Use `func` or `callback` with a quoted spec:
207
+
208
+ - Syntax: `func 'RET(TYPE name, TYPE name, ...){ ruby_code }'` (alias: `callback ...`)
209
+ - Inside the block, helper methods from `Libcall::Fiddley::DSL` are available:
210
+ - `int(ptr)`, `double(ptr)`, `cstr(ptr)` read values from pointers
211
+ - `read(:type, ptr)` reads any supported type; `ptr(addr)` makes a pointer
212
+
213
+ Quick examples
214
+
215
+ ```sh
216
+ # Fixture function: int32_t apply_i32(int32_t, int32_t, int32_t (*)(int32_t,int32_t))
217
+ libcall -ltest -L test/fixtures/libtest/build apply_i32 \
218
+ int 3 int 5 \
219
+ func 'int(int a,int b){ a + b }' \
220
+ -r i32
221
+ # => 8
222
+ ```
223
+
224
+ ```sh
225
+ # libc qsort: sort 4 ints ascending; use out:int[4] with an initializer so the result prints
226
+ libcall -lc qsort \
227
+ out:int[4] 4,2,3,1 \
228
+ size_t 4 \
229
+ size_t 4 \
230
+ callback 'int(void* a, void* b){ int(a) <=> int(b) }' \
231
+ -r void
232
+ # Output parameters:
233
+ # [0] int[4] = [1, 2, 3, 4]
234
+ ```
235
+
236
+ Notes
237
+
238
+ - Match the C signature exactly (types and arity). Blocks run in-process; exceptions abort the call.
239
+
163
240
  ## Warning
164
241
 
165
242
  FFI calls are inherently unsafe. You must:
@@ -171,6 +248,22 @@ FFI calls are inherently unsafe. You must:
171
248
 
172
249
  Incorrect usage can crash your program.
173
250
 
251
+ ## Windows Support
252
+
253
+ 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.
254
+
255
+ ### Windows Examples
256
+
257
+ ```powershell
258
+ # Calling C runtime functions
259
+ libcall msvcrt.dll sqrt double 16.0 -r f64 # => 4.0
260
+ ```
261
+
262
+ ```powershell
263
+ # Accessing environment variables
264
+ libcall msvcrt.dll getenv string "PATH" -r cstr
265
+ ```
266
+
174
267
  ## Development
175
268
 
176
269
  ```sh
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libcall
4
+ # Processes argument pairs for FFI calls
5
+ class ArgumentProcessor
6
+ # Value object to hold processed arguments
7
+ ProcessedArguments = Struct.new(
8
+ :arg_types,
9
+ :arg_values,
10
+ :out_refs,
11
+ :closures,
12
+ keyword_init: true
13
+ )
14
+
15
+ def initialize(arg_pairs)
16
+ @arg_pairs = arg_pairs
17
+ end
18
+
19
+ def process
20
+ arg_types = []
21
+ arg_values = []
22
+ out_refs = []
23
+ closures = []
24
+
25
+ @arg_pairs.each_with_index do |(type_sym, value), idx|
26
+ if type_sym.is_a?(Array)
27
+ process_complex_type(type_sym, value, idx, arg_types, arg_values, out_refs)
28
+ elsif type_sym == :callback
29
+ process_callback(value, arg_types, arg_values, closures)
30
+ else
31
+ arg_types << TypeMap.to_fiddle_type(type_sym)
32
+ arg_values << value
33
+ end
34
+ end
35
+
36
+ ProcessedArguments.new(
37
+ arg_types: arg_types,
38
+ arg_values: arg_values,
39
+ out_refs: out_refs,
40
+ closures: closures
41
+ )
42
+ end
43
+
44
+ private
45
+
46
+ def process_complex_type(type_sym, value, idx, arg_types, arg_values, out_refs)
47
+ case type_sym.first
48
+ when :out
49
+ process_output_pointer(type_sym, idx, arg_types, arg_values, out_refs)
50
+ when :array
51
+ process_input_array(type_sym, value, arg_types, arg_values)
52
+ when :out_array
53
+ process_output_array(type_sym, value, idx, arg_types, arg_values, out_refs)
54
+ else
55
+ raise Error, "Unknown array/output form: #{type_sym.inspect}"
56
+ end
57
+ end
58
+
59
+ def process_output_pointer(type_sym, idx, arg_types, arg_values, out_refs)
60
+ inner = type_sym[1]
61
+ ptr = TypeMap.allocate_output_pointer(inner)
62
+ out_refs << { index: idx, kind: :out, type: inner, ptr: ptr }
63
+ arg_types << TypeMap.to_fiddle_type(type_sym)
64
+ arg_values << ptr.to_i
65
+ end
66
+
67
+ def process_input_array(type_sym, value, arg_types, arg_values)
68
+ base = type_sym[1]
69
+ values = Array(value)
70
+ ptr = TypeMap.allocate_array(base, values.length)
71
+ TypeMap.write_array(ptr, base, values)
72
+ arg_types << TypeMap.to_fiddle_type(type_sym)
73
+ arg_values << ptr.to_i
74
+ end
75
+
76
+ def process_output_array(type_sym, value, idx, arg_types, arg_values, out_refs)
77
+ base = type_sym[1]
78
+ count = type_sym[2]
79
+ ptr = TypeMap.allocate_array(base, count)
80
+
81
+ # Optional initializer values
82
+ if value
83
+ vals = Array(value)
84
+ unless vals.length == count
85
+ raise Error,
86
+ "Initializer length #{vals.length} does not match out array size #{count}"
87
+ end
88
+
89
+ TypeMap.write_array(ptr, base, vals)
90
+ end
91
+
92
+ out_refs << { index: idx, kind: :out_array, base: base, count: count, ptr: ptr }
93
+ arg_types << TypeMap.to_fiddle_type(type_sym)
94
+ arg_values << ptr.to_i
95
+ end
96
+
97
+ def process_callback(value, arg_types, arg_values, closures)
98
+ closure = CallbackHandler.create(value)
99
+ closures << closure # keep alive during call
100
+ arg_types << TypeMap.to_fiddle_type(:callback)
101
+ arg_values << closure
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fiddle'
4
+
5
+ module Libcall
6
+ # Handles callback function pointer creation for FFI calls
7
+ class CallbackHandler
8
+ attr_reader :spec
9
+
10
+ def self.create(spec)
11
+ new(spec).create_closure
12
+ end
13
+
14
+ def initialize(spec)
15
+ @spec = spec
16
+ validate_spec!
17
+ end
18
+
19
+ def create_closure
20
+ ret_ty = TypeMap.to_fiddle_type(@spec[:ret])
21
+ arg_tys = @spec[:args].map { |a| TypeMap.to_fiddle_type(a) }
22
+ ruby_proc = build_proc
23
+
24
+ Fiddle::Closure::BlockCaller.new(ret_ty, arg_tys) do |*cb_args|
25
+ cooked_args = cook_arguments(cb_args)
26
+ ruby_proc.call(*cooked_args)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def validate_spec!
33
+ return if @spec.is_a?(Hash) && @spec[:kind] == :callback
34
+
35
+ raise Error, 'Invalid callback value; expected func signature and block'
36
+ end
37
+
38
+ def build_proc
39
+ ctx = Object.new.extend(Fiddley::DSL)
40
+ ctx.instance_eval("proc #{@spec[:block]}", __FILE__, __LINE__)
41
+ rescue SyntaxError => e
42
+ raise Error, "Invalid Ruby block for callback: #{e.message}"
43
+ end
44
+
45
+ def cook_arguments(cb_args)
46
+ cb_args.each_with_index.map do |v, i|
47
+ at = @spec[:args][i]
48
+ at == :voidp ? Fiddle::Pointer.new(v) : v
49
+ end
50
+ end
51
+ end
52
+ end
@@ -15,53 +15,17 @@ module Libcall
15
15
  end
16
16
 
17
17
  def call
18
- arg_types = []
19
- arg_values = []
20
- out_refs = []
18
+ processor = ArgumentProcessor.new(arg_pairs)
19
+ processed = processor.process
21
20
 
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
50
- end
51
-
52
- ret_type = TypeMap.to_fiddle_type(return_type)
53
-
54
- handle = Fiddle.dlopen(lib_path)
55
- func_ptr = handle[func_name]
56
- func = Fiddle::Function.new(func_ptr, arg_types, ret_type)
57
-
58
- raw_result = func.call(*arg_values)
21
+ raw_result = execute_function(processed)
59
22
  formatted_result = format_result(raw_result, return_type)
60
23
 
61
- if out_refs.empty?
24
+ output_reader = OutputReader.new(processed.out_refs)
25
+ if output_reader.empty?
62
26
  formatted_result
63
27
  else
64
- { result: formatted_result, outputs: read_output_values(out_refs) }
28
+ { result: formatted_result, outputs: output_reader.read }
65
29
  end
66
30
  rescue Fiddle::DLError => e
67
31
  raise Error, "Failed to load library or function: #{e.message}"
@@ -69,6 +33,14 @@ module Libcall
69
33
 
70
34
  private
71
35
 
36
+ def execute_function(processed)
37
+ handle = Fiddle.dlopen(lib_path)
38
+ func_ptr = handle[func_name]
39
+ ret_type = TypeMap.to_fiddle_type(return_type)
40
+ func = Fiddle::Function.new(func_ptr, processed.arg_types, ret_type)
41
+ func.call(*processed.arg_values)
42
+ end
43
+
72
44
  def format_result(result, type)
73
45
  case type
74
46
  when :void
@@ -90,22 +62,5 @@ module Libcall
90
62
  result
91
63
  end
92
64
  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
110
65
  end
111
66
  end
data/lib/libcall/cli.rb CHANGED
@@ -85,6 +85,11 @@ module Libcall
85
85
  end
86
86
 
87
87
  <<~BANNER
88
+
89
+ Program: libcall (Call C functions from the command line)
90
+ Version: #{Libcall::VERSION}
91
+ Source: https://github.com/kojix2/libcall
92
+
88
93
  Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
89
94
 
90
95
  Call C functions in shared libraries from the command line.
@@ -95,14 +100,14 @@ module Libcall
95
100
  #{examples.lines.map { |line| line.chomp }.join("\n ")}
96
101
 
97
102
  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
103
+ -l, --lib LIBRARY Library name to search for (e.g., -lm for libm)
104
+ -L, --lib-path PATH Add directory to library search path
105
+ -r, --ret TYPE Return type (default: void)
106
+ --dry-run Show what would be executed without calling
107
+ --json Output result in JSON format
108
+ --verbose Show detailed information
109
+ -h, --help Show this help message
110
+ -v, --version Show version information
106
111
  BANNER
107
112
  end
108
113
 
@@ -156,8 +161,17 @@ module Libcall
156
161
  type_sym = Parser.parse_type(type_tok)
157
162
 
158
163
  # TYPE that represents an output pointer/array does not require a value
164
+ # For out:TYPE[N], allow an optional comma-separated initializer list right after the type.
159
165
  if type_sym.is_a?(Array) && %i[out out_array].include?(type_sym.first)
160
- arg_pairs << [type_sym, nil]
166
+ if type_sym.first == :out_array && i < argv.length && argv[i].include?(',')
167
+ init_tok = argv[i]
168
+ i += 1
169
+ base = type_sym[1]
170
+ values = Parser.coerce_value([:array, base], init_tok)
171
+ arg_pairs << [type_sym, values]
172
+ else
173
+ arg_pairs << [type_sym, nil]
174
+ end
161
175
  next
162
176
  end
163
177
 
@@ -0,0 +1,140 @@
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
+ # Return size in bytes for a given type symbol.
48
+
49
+ def sizeof(type)
50
+ Libcall::TypeMap.sizeof(type)
51
+ rescue StandardError => e
52
+ raise Libcall::Error, "unknown type for sizeof: #{type} (#{e.message})"
53
+ end
54
+
55
+ # Convert a type symbol to a Fiddle type constant.
56
+ def to_fiddle_type(type)
57
+ Libcall::TypeMap.to_fiddle_type(type)
58
+ rescue StandardError => e
59
+ raise Libcall::Error, "unknown type for to_fiddle_type: #{type} (#{e.message})"
60
+ end
61
+
62
+ # Pack template for array values of given base type.
63
+ def array_pack_template(type)
64
+ Libcall::TypeMap.pack_template(type)
65
+ rescue StandardError => e
66
+ raise Libcall::Error, "Unsupported array base type: #{type} (#{e.message})"
67
+ end
68
+
69
+ # Convert Ruby array of numbers to a binary string for the given type
70
+ def array2str(type, array)
71
+ array.pack("#{array_pack_template(type)}*")
72
+ end
73
+
74
+ # Convert binary string to Ruby array of the given type
75
+ def str2array(type, str)
76
+ str.unpack("#{array_pack_template(type)}*")
77
+ end
78
+ end
79
+
80
+ # Minimal memory buffer wrapper to make building args/arrays convenient
81
+ class MemoryPointer
82
+ attr_reader :size
83
+
84
+ def initialize(type, count = 1)
85
+ @type = type
86
+ @size = Utils.sizeof(type) * count
87
+ @ptr = Fiddle::Pointer.malloc(@size)
88
+ end
89
+
90
+ def to_ptr
91
+ @ptr
92
+ end
93
+
94
+ def address
95
+ @ptr.to_i
96
+ end
97
+
98
+ def write_array(type, values)
99
+ data = Utils.array2str(type, Array(values))
100
+ @ptr[0, data.bytesize] = data
101
+ self
102
+ end
103
+
104
+ def read_array(type, count)
105
+ bytes = Utils.sizeof(type) * count
106
+ Utils.str2array(type, @ptr[0, bytes])
107
+ end
108
+
109
+ def put_bytes(offset, str)
110
+ @ptr[offset, str.bytesize] = str
111
+ end
112
+
113
+ def write_bytes(str)
114
+ put_bytes(0, str)
115
+ end
116
+
117
+ def get_bytes(offset, len)
118
+ @ptr[offset, len]
119
+ end
120
+
121
+ def read_bytes(len)
122
+ get_bytes(0, len)
123
+ end
124
+
125
+ # Return Fiddle::Pointer stored at this pointer (read void*)
126
+ def read_pointer
127
+ to_ptr.ptr
128
+ end
129
+ end
130
+
131
+ # Wrap Fiddle::Closure::BlockCaller with friendlier type mapping
132
+ class Function < Fiddle::Closure::BlockCaller
133
+ def initialize(ret, params, &blk)
134
+ r = Utils.to_fiddle_type(ret)
135
+ p = Array(params).map { |t| Utils.to_fiddle_type(t) }
136
+ super(r, p, &blk)
137
+ end
138
+ end
139
+ end
140
+ end
@@ -19,6 +19,9 @@ module Libcall
19
19
 
20
20
  # Find library by name (e.g., "m" -> "/lib/x86_64-linux-gnu/libm.so.6")
21
21
  def find(lib_name)
22
+ # Normalize "-lfoo" style if passed as is (harmless if not present)
23
+ lib_name = lib_name.sub(/\A-l/, '')
24
+
22
25
  # If it's a path, return as-is
23
26
  return File.expand_path(lib_name) if path_like?(lib_name)
24
27
 
@@ -52,6 +55,30 @@ module Libcall
52
55
  resolved = resolve_by_name_in_paths(lib_name, search_paths)
53
56
  return resolved if resolved
54
57
 
58
+ # On macOS, try common library naming conventions for dyld shared cache
59
+ # (e.g., libSystem.B.dylib exists in cache but not on filesystem)
60
+ if Platform.darwin?
61
+ prefixes = lib_name.start_with?('lib') ? [''] : ['lib', '']
62
+ extensions = Platform.library_extensions
63
+
64
+ prefixes.product(extensions).each do |prefix, ext|
65
+ candidates = [
66
+ "#{prefix}#{lib_name}#{ext}",
67
+ "#{prefix}#{lib_name}.B#{ext}", # Common pattern: libSystem.B.dylib
68
+ "#{prefix}#{lib_name}.A#{ext}" # Also try .A variant
69
+ ]
70
+
71
+ candidates.each do |candidate|
72
+ # Test if dyld can resolve this name
73
+
74
+ Fiddle.dlopen(candidate)
75
+ return candidate # Return the name itself, let Fiddle resolve it
76
+ rescue Fiddle::DLError
77
+ next
78
+ end
79
+ end
80
+ end
81
+
55
82
  raise Error, "Library not found: #{lib_name} (searched in: #{search_paths.join(', ')})"
56
83
  end
57
84
 
@@ -134,11 +161,20 @@ module Libcall
134
161
  full_path = File.join(path, name)
135
162
  return File.expand_path(full_path) if File.file?(full_path)
136
163
 
137
- # Check for versioned libraries (libm.so.6, etc.)
164
+ # Check for versioned libraries:
165
+ # - Linux: libm.so.6 => "#{name}.*"
166
+ # - macOS: libSystem.B.dylib, libcrypto.3.dylib => "#{prefix}#{lib_name}.*#{ext}"
138
167
  next if ext.empty?
139
168
 
140
- matches = Dir.glob(File.join(path, "#{name}.*")).select { |f| File.file?(f) }
141
- return File.expand_path(matches.first) unless matches.empty?
169
+ patterns = [
170
+ File.join(path, "#{name}.*"),
171
+ File.join(path, "#{prefix}#{lib_name}.*#{ext}")
172
+ ]
173
+
174
+ patterns.each do |pattern|
175
+ matches = Dir.glob(pattern).select { |f| File.file?(f) }
176
+ return File.expand_path(matches.first) unless matches.empty?
177
+ end
142
178
  end
143
179
 
144
180
  nil
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Libcall
4
+ # Handles reading output parameters after FFI calls
5
+ class OutputReader
6
+ def initialize(out_refs)
7
+ @out_refs = out_refs
8
+ end
9
+
10
+ def read
11
+ @out_refs.map do |ref|
12
+ case ref[:kind]
13
+ when :out
14
+ read_single_output(ref)
15
+ when :out_array
16
+ read_array_output(ref)
17
+ else
18
+ raise Error, "Unknown out reference kind: #{ref[:kind]}"
19
+ end
20
+ end
21
+ end
22
+
23
+ def empty?
24
+ @out_refs.empty?
25
+ end
26
+
27
+ private
28
+
29
+ def read_single_output(ref)
30
+ value = TypeMap.read_output_pointer(ref[:ptr], ref[:type])
31
+ { index: ref[:index], type: ref[:type].to_s, value: value }
32
+ end
33
+
34
+ def read_array_output(ref)
35
+ values = TypeMap.read_array(ref[:ptr], ref[:base], ref[:count])
36
+ {
37
+ index: ref[:index],
38
+ type: "#{ref[:base]}[#{ref[:count]}]",
39
+ value: values
40
+ }
41
+ end
42
+ end
43
+ end
@@ -7,6 +7,9 @@ module Libcall
7
7
  class Parser
8
8
  # Pair-only API helpers
9
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
+
10
13
  # Output array spec: out:TYPE[N]
11
14
  if type_str.start_with?('out:') && type_str.match(/^out:(.+)\[(\d+)\]$/)
12
15
  base = Regexp.last_match(1)
@@ -51,6 +54,52 @@ module Libcall
51
54
  end
52
55
 
53
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, args_s, block_src = m.captures.map(&:strip)
64
+ if block_src =~ /\{\s*\|/
65
+ raise Error,
66
+ 'Explicit block parameters are not supported; name your C args and omit block params (e.g., int(int a,int b){a+b})'
67
+ end
68
+
69
+ ret_sym = TypeMap.lookup(ret_s)
70
+ raise Error, "Unknown callback return type: #{ret_s}" unless ret_sym
71
+
72
+ pairs = if args_s.empty?
73
+ []
74
+ else
75
+ args_s.split(',').map do |raw|
76
+ # Normalize pointer syntax: "void *a", "void * a", "void*a" → "void* a"
77
+ normalized = raw.strip.gsub(/\s*\*\s*/, '* ').strip
78
+
79
+ if (mm = normalized.match(/\A(.+?)\s+([A-Za-z_][A-Za-z0-9_]*)\z/))
80
+ type_part = mm[1].strip
81
+ name_part = mm[2]
82
+ else
83
+ type_part = normalized
84
+ name_part = nil
85
+ end
86
+ sym = TypeMap.lookup(type_part)
87
+ raise Error, "Unknown callback arg type: #{raw.strip}" unless sym
88
+
89
+ [sym, name_part]
90
+ end
91
+ end
92
+
93
+ arg_syms = pairs.map(&:first)
94
+ names_with_nils = pairs.map(&:last)
95
+ if !names_with_nils.empty? && names_with_nils.all?
96
+ block_src = block_src.sub(/\{\s*/) do
97
+ "{|#{names_with_nils.join(',')}| "
98
+ end
99
+ end
100
+
101
+ return { kind: :callback, ret: ret_sym, args: arg_syms, block: block_src }
102
+ end
54
103
  # Input array values: comma-separated
55
104
  if type_sym.is_a?(Array) && type_sym.first == :array
56
105
  base = type_sym[1]
@@ -27,6 +27,7 @@ module Libcall
27
27
  'void' => :void,
28
28
  # Common C type names
29
29
  'char' => :char,
30
+ 'uchar' => :uchar,
30
31
  'short' => :short,
31
32
  'ushort' => :ushort,
32
33
  'int' => :int,
@@ -35,6 +36,21 @@ module Libcall
35
36
  'ulong' => :ulong,
36
37
  'float' => :float,
37
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,
38
54
  # Extended type names (stdint-like)
39
55
  'int8' => :char,
40
56
  'uint8' => :uchar,
@@ -46,6 +62,15 @@ module Libcall
46
62
  'uint64' => :ulong_long,
47
63
  'float32' => :float,
48
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,
49
74
  # Size and pointer-sized integers
50
75
  'size_t' => :ulong,
51
76
  'ssize_t' => :long,
@@ -96,18 +121,32 @@ module Libcall
96
121
  return Fiddle::TYPE_VOIDP if %i[out array out_array].include?(tag)
97
122
  end
98
123
 
124
+ # Callback function pointers are passed as void*
125
+ return Fiddle::TYPE_VOIDP if type_sym == :callback
126
+
99
127
  case type_sym
128
+ # Void type
100
129
  when :void then Fiddle::TYPE_VOID
130
+ # Integer types (8-bit)
101
131
  when :char then Fiddle::TYPE_CHAR
102
132
  when :uchar then Fiddle::TYPE_UCHAR
133
+ # Integer types (16-bit)
103
134
  when :short then Fiddle::TYPE_SHORT
104
135
  when :ushort then Fiddle::TYPE_USHORT
136
+ # Integer types (32-bit)
105
137
  when :int, :uint then Fiddle::TYPE_INT
138
+ # Integer types (platform-dependent)
106
139
  when :long, :ulong then Fiddle::TYPE_LONG
140
+ # Integer types (64-bit)
107
141
  when :long_long, :ulong_long then Fiddle::TYPE_LONG_LONG
142
+ # Floating point types
108
143
  when :float then Fiddle::TYPE_FLOAT
109
144
  when :double then Fiddle::TYPE_DOUBLE
110
- when :voidp, :string then Fiddle::TYPE_VOIDP
145
+ # Pointer types
146
+ when :voidp, :pointer then Fiddle::TYPE_VOIDP
147
+ when :string then Fiddle::TYPE_VOIDP
148
+ # Special types
149
+ when :size_t then Fiddle::TYPE_SIZE_T
111
150
  else
112
151
  raise Error, "Unknown Fiddle type: #{type_sym}"
113
152
  end
@@ -116,14 +155,23 @@ module Libcall
116
155
  # Get the size in bytes for a type symbol
117
156
  def self.sizeof(type_sym)
118
157
  case type_sym
158
+ # Integer types (8-bit)
119
159
  when :char, :uchar then Fiddle::SIZEOF_CHAR
160
+ # Integer types (16-bit)
120
161
  when :short, :ushort then Fiddle::SIZEOF_SHORT
162
+ # Integer types (32-bit)
121
163
  when :int, :uint then Fiddle::SIZEOF_INT
164
+ # Integer types (platform-dependent)
122
165
  when :long, :ulong then Fiddle::SIZEOF_LONG
166
+ # Integer types (64-bit)
123
167
  when :long_long, :ulong_long then Fiddle::SIZEOF_LONG_LONG
168
+ # Floating point types
124
169
  when :float then Fiddle::SIZEOF_FLOAT
125
170
  when :double then Fiddle::SIZEOF_DOUBLE
126
- when :voidp, :string then Fiddle::SIZEOF_VOIDP
171
+ # Pointer types
172
+ when :voidp, :pointer, :string then Fiddle::SIZEOF_VOIDP
173
+ # Special types
174
+ when :size_t then Fiddle::SIZEOF_SIZE_T
127
175
  else
128
176
  raise Error, "Cannot get size for type: #{type_sym}"
129
177
  end
@@ -140,18 +188,26 @@ module Libcall
140
188
  # Read value from output pointer
141
189
  def self.read_output_pointer(ptr, type_sym)
142
190
  case type_sym
191
+ # Integer types (8-bit)
143
192
  when :char then ptr[0, Fiddle::SIZEOF_CHAR].unpack1('c')
144
193
  when :uchar then ptr[0, Fiddle::SIZEOF_CHAR].unpack1('C')
194
+ # Integer types (16-bit)
145
195
  when :short then ptr[0, Fiddle::SIZEOF_SHORT].unpack1('s')
146
196
  when :ushort then ptr[0, Fiddle::SIZEOF_SHORT].unpack1('S')
197
+ # Integer types (32-bit)
147
198
  when :int then ptr[0, Fiddle::SIZEOF_INT].unpack1('i')
148
199
  when :uint then ptr[0, Fiddle::SIZEOF_INT].unpack1('I')
200
+ # Integer types (platform-dependent)
149
201
  when :long then ptr[0, Fiddle::SIZEOF_LONG].unpack1('l!')
150
202
  when :ulong then ptr[0, Fiddle::SIZEOF_LONG].unpack1('L!')
203
+ # Integer types (64-bit)
151
204
  when :long_long then ptr[0, Fiddle::SIZEOF_LONG_LONG].unpack1('q')
152
205
  when :ulong_long then ptr[0, Fiddle::SIZEOF_LONG_LONG].unpack1('Q')
206
+ # Floating point types
153
207
  when :float then ptr[0, Fiddle::SIZEOF_FLOAT].unpack1('f')
154
208
  when :double then ptr[0, Fiddle::SIZEOF_DOUBLE].unpack1('d')
209
+ # Pointer types
210
+ when :voidp then format('0x%x', ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J'))
155
211
  when :string
156
212
  addr = ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J')
157
213
  return '(null)' if addr.zero?
@@ -161,12 +217,16 @@ module Libcall
161
217
  rescue StandardError
162
218
  format('0x%x', addr)
163
219
  end
164
- when :voidp then format('0x%x', ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J'))
165
220
  else
166
221
  raise Error, "Cannot read output value for type: #{type_sym}"
167
222
  end
168
223
  end
169
224
 
225
+ # Read a single scalar value at address (helper for callbacks)
226
+ def self.read_scalar(ptr, type_sym)
227
+ read_output_pointer(ptr, type_sym)
228
+ end
229
+
170
230
  # Allocate memory for an array of base type and count elements
171
231
  def self.allocate_array(base_type, count)
172
232
  Fiddle::Pointer.malloc(sizeof(base_type) * count)
@@ -189,18 +249,28 @@ module Libcall
189
249
 
190
250
  def self.pack_template(base_type)
191
251
  case base_type
252
+ # Integer types (8-bit)
192
253
  when :char then 'c'
193
254
  when :uchar then 'C'
255
+ # Integer types (16-bit)
194
256
  when :short then 's'
195
257
  when :ushort then 'S'
258
+ # Integer types (32-bit)
196
259
  when :int then 'i'
197
260
  when :uint then 'I'
261
+ # Integer types (platform-dependent)
198
262
  when :long then 'l!'
199
263
  when :ulong then 'L!'
264
+ # Integer types (64-bit)
200
265
  when :long_long then 'q'
201
266
  when :ulong_long then 'Q'
267
+ # Floating point types
202
268
  when :float then 'f'
203
269
  when :double then 'd'
270
+ # Pointer types
271
+ when :pointer, :voidp then 'J'
272
+ # Special types
273
+ when :size_t then (Fiddle::SIZEOF_VOIDP == Fiddle::SIZEOF_LONG ? 'L!' : 'Q')
204
274
  else
205
275
  raise Error, "Unsupported array base type: #{base_type}"
206
276
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Libcall
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.4'
5
5
  end
data/lib/libcall.rb CHANGED
@@ -3,8 +3,12 @@
3
3
  require_relative 'libcall/version'
4
4
  require_relative 'libcall/parser'
5
5
  require_relative 'libcall/library_finder'
6
+ require_relative 'libcall/callback_handler'
7
+ require_relative 'libcall/output_reader'
8
+ require_relative 'libcall/argument_processor'
6
9
  require_relative 'libcall/caller'
7
10
  require_relative 'libcall/cli'
11
+ require_relative 'libcall/fiddley'
8
12
 
9
13
  module Libcall
10
14
  class Error < StandardError; end
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.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - kojix2
@@ -47,9 +47,13 @@ files:
47
47
  - README.md
48
48
  - exe/libcall
49
49
  - lib/libcall.rb
50
+ - lib/libcall/argument_processor.rb
51
+ - lib/libcall/callback_handler.rb
50
52
  - lib/libcall/caller.rb
51
53
  - lib/libcall/cli.rb
54
+ - lib/libcall/fiddley.rb
52
55
  - lib/libcall/library_finder.rb
56
+ - lib/libcall/output_reader.rb
53
57
  - lib/libcall/parser.rb
54
58
  - lib/libcall/platform.rb
55
59
  - lib/libcall/type_map.rb