libcall 0.0.3 → 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: db2f2234f00412d76e307fc12179b0a829953a08bd6aefa83d59bbab866e85f3
4
- data.tar.gz: bb2cd9b42fb09fc6b6e5201ed98c16dae47258e9a1fb23ef76fc6a49efe17ab3
3
+ metadata.gz: 6d3e1c356b60aca8356cd319a9c31619850cb4cbee3275d94f04bc0f3d18b422
4
+ data.tar.gz: 75ebf8ecc71c6d38f34c4b1f1b6be809e95cc2d5f24cba21755cbf3455ccd9fb
5
5
  SHA512:
6
- metadata.gz: 12bc58798f6130a316ba87e53a66d2806035a7fd8abbb6ba2fc71654d82e0f2210149152cf9876ca818a5241808a2c871021eca2c7264a2110cfe37199da2872
7
- data.tar.gz: bb6f252e8a7a9aec0a353abec0ed9674b346bb1af3471768ca51fad040234024c6fc77b18a25d42c396a389a0063ac44c5675f54fa8bd66eed3b0c9055da8e34
6
+ metadata.gz: fcebbe2dd033a7392ee4bc5b5efed51e5352f6cc1a1dee9aa33751e9833703a6574853265698851548c1b1cda17fad437f450f6058b2a664b7b599c049b44e48
7
+ data.tar.gz: f3f36070e3aec368a843d72851c88bc25e86a8f04bc23e4950e92e0eda46a23a8980d25598c697069c093f3083b0476d2f6bd1d082ec2dc46e64dd1af661fe6e
data/README.md CHANGED
@@ -12,14 +12,6 @@ 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
@@ -30,30 +22,10 @@ libcall -lm sqrt double 16 -r double # => 4.0
30
22
  libcall -lc strlen string "hello" -r usize # => 5
31
23
  ```
32
24
 
33
- ### Argument Syntax
34
-
35
- Pass arguments as TYPE VALUE pairs (single-token suffix style has been removed):
36
-
37
- - Examples: `int 10`, `double -3.14`, `string "hello"`
38
- - Negative values are safe (not treated as options): `int -23`
39
-
40
- Pointers and null:
41
-
42
- - Use `ptr` (or `pointer`) to pass raw addresses as integers
43
- - Use `null`, `nil`, `NULL`, or `0` to pass a null pointer
44
-
45
- ```sh
46
- # Pass a null pointer to a function taking const char*
47
- libcall -ltest str_length ptr null -r i32
48
- # => 0
49
- ```
50
-
51
- End of options `--`:
52
-
53
- - Use `--` to stop option parsing if a value starts with `-`
25
+ ## Usage
54
26
 
55
27
  ```sh
56
- libcall -lc getenv string -- -r -r cstr
28
+ libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
57
29
  ```
58
30
 
59
31
  ### Options
@@ -74,12 +46,6 @@ Library search:
74
46
 
75
47
  ### More Examples
76
48
 
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
49
  Output parameter with libm
84
50
 
85
51
  ```sh
@@ -110,40 +76,89 @@ libcall --dry-run -lc getpid -r int
110
76
  # Return: int
111
77
  ```
112
78
 
113
- Windows: calling C runtime functions
79
+ ## Type Reference
114
80
 
115
- ```powershell
116
- libcall msvcrt.dll sqrt double 16.0 -r f64 # => 4.0
117
- ```
81
+ libcall supports multiple naming conventions for types, making it easy to work with C libraries.
118
82
 
119
- Windows: accessing environment variables
83
+ ### Integer Types
120
84
 
121
- ```powershell
122
- 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**)
123
134
  ```
124
135
 
125
- ## 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` |
143
+
144
+ ### Callback Types
126
145
 
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 |
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 |
139
150
 
140
- Also supported:
151
+ ### Argument Syntax
152
+
153
+ Pass arguments as TYPE VALUE pairs:
141
154
 
142
- - `string`: C string argument (char\*)
143
- - `cstr`: C string return (char\*)
144
- - `ptr`/`pointer`: void\* pointer
155
+ ```sh
156
+ libcall -lm sqrt double 16.0 -r f64
157
+ libcall -lc strlen string "hello" -r usize
158
+ ```
145
159
 
146
- See [type_map.rb](lib/libcall/type_map.rb) for all available type mappings.
160
+ - Null pointers: Use `null`, `NULL`, `nil`, or `0`
161
+ - Negative numbers work as expected (e.g., `double -3.14`)
147
162
 
148
163
  ## pkg-config Support
149
164
 
@@ -190,7 +205,7 @@ libcall -lSystem arc4random_buf out:uchar[16] size_t 16 -r void
190
205
 
191
206
  Pass a C function pointer via a Ruby callback. Use `func` or `callback` with a quoted spec:
192
207
 
193
- - Syntax: `func 'RET(ARG,ARG,...){|a, b, ...| ruby_code }'` (alias: `callback ...`)
208
+ - Syntax: `func 'RET(TYPE name, TYPE name, ...){ ruby_code }'` (alias: `callback ...`)
194
209
  - Inside the block, helper methods from `Libcall::Fiddley::DSL` are available:
195
210
  - `int(ptr)`, `double(ptr)`, `cstr(ptr)` read values from pointers
196
211
  - `read(:type, ptr)` reads any supported type; `ptr(addr)` makes a pointer
@@ -201,7 +216,7 @@ Quick examples
201
216
  # Fixture function: int32_t apply_i32(int32_t, int32_t, int32_t (*)(int32_t,int32_t))
202
217
  libcall -ltest -L test/fixtures/libtest/build apply_i32 \
203
218
  int 3 int 5 \
204
- func 'int(int,int){|a,b| a + b}' \
219
+ func 'int(int a,int b){ a + b }' \
205
220
  -r i32
206
221
  # => 8
207
222
  ```
@@ -212,7 +227,7 @@ libcall -lc qsort \
212
227
  out:int[4] 4,2,3,1 \
213
228
  size_t 4 \
214
229
  size_t 4 \
215
- callback 'int(void*,void*){|pa,pb| int(pa) <=> int(pb) }' \
230
+ callback 'int(void* a, void* b){ int(a) <=> int(b) }' \
216
231
  -r void
217
232
  # Output parameters:
218
233
  # [0] int[4] = [1, 2, 3, 4]
@@ -233,6 +248,22 @@ FFI calls are inherently unsafe. You must:
233
248
 
234
249
  Incorrect usage can crash your program.
235
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
+
236
267
  ## Development
237
268
 
238
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,90 +15,17 @@ module Libcall
15
15
  end
16
16
 
17
17
  def call
18
- arg_types = []
19
- arg_values = []
20
- out_refs = []
21
- closures = []
18
+ processor = ArgumentProcessor.new(arg_pairs)
19
+ processed = processor.process
22
20
 
23
- arg_pairs.each_with_index do |(type_sym, value), idx|
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
84
- else
85
- arg_values << value
86
- end
87
- end
88
-
89
- ret_type = TypeMap.to_fiddle_type(return_type)
90
-
91
- handle = Fiddle.dlopen(lib_path)
92
- func_ptr = handle[func_name]
93
- func = Fiddle::Function.new(func_ptr, arg_types, ret_type)
94
-
95
- raw_result = func.call(*arg_values)
21
+ raw_result = execute_function(processed)
96
22
  formatted_result = format_result(raw_result, return_type)
97
23
 
98
- if out_refs.empty?
24
+ output_reader = OutputReader.new(processed.out_refs)
25
+ if output_reader.empty?
99
26
  formatted_result
100
27
  else
101
- { result: formatted_result, outputs: read_output_values(out_refs) }
28
+ { result: formatted_result, outputs: output_reader.read }
102
29
  end
103
30
  rescue Fiddle::DLError => e
104
31
  raise Error, "Failed to load library or function: #{e.message}"
@@ -106,6 +33,14 @@ module Libcall
106
33
 
107
34
  private
108
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
+
109
44
  def format_result(result, type)
110
45
  case type
111
46
  when :void
@@ -127,22 +62,5 @@ module Libcall
127
62
  result
128
63
  end
129
64
  end
130
-
131
- def read_output_values(out_refs)
132
- out_refs.map do |ref|
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
145
- end
146
- end
147
65
  end
148
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
 
@@ -44,42 +44,26 @@ module Libcall
44
44
  module Utils
45
45
  module_function
46
46
 
47
- # Native size_t pack template and size
48
- SIZET_PACK = (Fiddle::SIZEOF_VOIDP == Fiddle::SIZEOF_LONG ? 'L!' : 'Q')
49
-
50
47
  # 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
48
 
56
- # Delegate to Libcall::TypeMap when possible
49
+ def sizeof(type)
57
50
  Libcall::TypeMap.sizeof(type)
58
- rescue StandardError
59
- raise Libcall::Error, "unknown type for sizeof: #{type}"
51
+ rescue StandardError => e
52
+ raise Libcall::Error, "unknown type for sizeof: #{type} (#{e.message})"
60
53
  end
61
54
 
62
55
  # Convert a type symbol to a Fiddle type constant.
63
56
  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
57
  Libcall::TypeMap.to_fiddle_type(type)
68
- rescue StandardError
69
- raise Libcall::Error, "unknown type for to_fiddle_type: #{type}"
58
+ rescue StandardError => e
59
+ raise Libcall::Error, "unknown type for to_fiddle_type: #{type} (#{e.message})"
70
60
  end
71
61
 
72
62
  # Pack template for array values of given base type.
73
63
  def array_pack_template(type)
74
- return SIZET_PACK if type == :size_t
75
-
76
- # Use TypeMap's packing for standard types
77
64
  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}"
65
+ rescue StandardError => e
66
+ raise Libcall::Error, "Unsupported array base type: #{type} (#{e.message})"
83
67
  end
84
68
 
85
69
  # Convert Ruby array of numbers to a binary string for the given type
@@ -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
@@ -60,23 +60,43 @@ module Libcall
60
60
  m = src.match(/\A\s*([^(\s]+)\s*\(([^)]*)\)\s*(\{.*\})\s*\z/m)
61
61
  raise Error, "Invalid callback spec: #{src}" unless m
62
62
 
63
- ret_s = m[1].strip
64
- args_s = m[2].strip
65
- block_src = m[3]
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
66
68
 
67
69
  ret_sym = TypeMap.lookup(ret_s)
68
70
  raise Error, "Unknown callback return type: #{ret_s}" unless ret_sym
69
71
 
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
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
80
100
 
81
101
  return { kind: :callback, ret: ret_sym, args: arg_syms, block: block_src }
82
102
  end
@@ -125,17 +125,28 @@ module Libcall
125
125
  return Fiddle::TYPE_VOIDP if type_sym == :callback
126
126
 
127
127
  case type_sym
128
+ # Void type
128
129
  when :void then Fiddle::TYPE_VOID
130
+ # Integer types (8-bit)
129
131
  when :char then Fiddle::TYPE_CHAR
130
132
  when :uchar then Fiddle::TYPE_UCHAR
133
+ # Integer types (16-bit)
131
134
  when :short then Fiddle::TYPE_SHORT
132
135
  when :ushort then Fiddle::TYPE_USHORT
136
+ # Integer types (32-bit)
133
137
  when :int, :uint then Fiddle::TYPE_INT
138
+ # Integer types (platform-dependent)
134
139
  when :long, :ulong then Fiddle::TYPE_LONG
140
+ # Integer types (64-bit)
135
141
  when :long_long, :ulong_long then Fiddle::TYPE_LONG_LONG
142
+ # Floating point types
136
143
  when :float then Fiddle::TYPE_FLOAT
137
144
  when :double then Fiddle::TYPE_DOUBLE
138
- 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
139
150
  else
140
151
  raise Error, "Unknown Fiddle type: #{type_sym}"
141
152
  end
@@ -144,14 +155,23 @@ module Libcall
144
155
  # Get the size in bytes for a type symbol
145
156
  def self.sizeof(type_sym)
146
157
  case type_sym
158
+ # Integer types (8-bit)
147
159
  when :char, :uchar then Fiddle::SIZEOF_CHAR
160
+ # Integer types (16-bit)
148
161
  when :short, :ushort then Fiddle::SIZEOF_SHORT
162
+ # Integer types (32-bit)
149
163
  when :int, :uint then Fiddle::SIZEOF_INT
164
+ # Integer types (platform-dependent)
150
165
  when :long, :ulong then Fiddle::SIZEOF_LONG
166
+ # Integer types (64-bit)
151
167
  when :long_long, :ulong_long then Fiddle::SIZEOF_LONG_LONG
168
+ # Floating point types
152
169
  when :float then Fiddle::SIZEOF_FLOAT
153
170
  when :double then Fiddle::SIZEOF_DOUBLE
154
- 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
155
175
  else
156
176
  raise Error, "Cannot get size for type: #{type_sym}"
157
177
  end
@@ -168,18 +188,26 @@ module Libcall
168
188
  # Read value from output pointer
169
189
  def self.read_output_pointer(ptr, type_sym)
170
190
  case type_sym
191
+ # Integer types (8-bit)
171
192
  when :char then ptr[0, Fiddle::SIZEOF_CHAR].unpack1('c')
172
193
  when :uchar then ptr[0, Fiddle::SIZEOF_CHAR].unpack1('C')
194
+ # Integer types (16-bit)
173
195
  when :short then ptr[0, Fiddle::SIZEOF_SHORT].unpack1('s')
174
196
  when :ushort then ptr[0, Fiddle::SIZEOF_SHORT].unpack1('S')
197
+ # Integer types (32-bit)
175
198
  when :int then ptr[0, Fiddle::SIZEOF_INT].unpack1('i')
176
199
  when :uint then ptr[0, Fiddle::SIZEOF_INT].unpack1('I')
200
+ # Integer types (platform-dependent)
177
201
  when :long then ptr[0, Fiddle::SIZEOF_LONG].unpack1('l!')
178
202
  when :ulong then ptr[0, Fiddle::SIZEOF_LONG].unpack1('L!')
203
+ # Integer types (64-bit)
179
204
  when :long_long then ptr[0, Fiddle::SIZEOF_LONG_LONG].unpack1('q')
180
205
  when :ulong_long then ptr[0, Fiddle::SIZEOF_LONG_LONG].unpack1('Q')
206
+ # Floating point types
181
207
  when :float then ptr[0, Fiddle::SIZEOF_FLOAT].unpack1('f')
182
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'))
183
211
  when :string
184
212
  addr = ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J')
185
213
  return '(null)' if addr.zero?
@@ -189,7 +217,6 @@ module Libcall
189
217
  rescue StandardError
190
218
  format('0x%x', addr)
191
219
  end
192
- when :voidp then format('0x%x', ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J'))
193
220
  else
194
221
  raise Error, "Cannot read output value for type: #{type_sym}"
195
222
  end
@@ -222,18 +249,28 @@ module Libcall
222
249
 
223
250
  def self.pack_template(base_type)
224
251
  case base_type
252
+ # Integer types (8-bit)
225
253
  when :char then 'c'
226
254
  when :uchar then 'C'
255
+ # Integer types (16-bit)
227
256
  when :short then 's'
228
257
  when :ushort then 'S'
258
+ # Integer types (32-bit)
229
259
  when :int then 'i'
230
260
  when :uint then 'I'
261
+ # Integer types (platform-dependent)
231
262
  when :long then 'l!'
232
263
  when :ulong then 'L!'
264
+ # Integer types (64-bit)
233
265
  when :long_long then 'q'
234
266
  when :ulong_long then 'Q'
267
+ # Floating point types
235
268
  when :float then 'f'
236
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')
237
274
  else
238
275
  raise Error, "Unsupported array base type: #{base_type}"
239
276
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Libcall
4
- VERSION = '0.0.3'
4
+ VERSION = '0.0.4'
5
5
  end
data/lib/libcall.rb CHANGED
@@ -3,6 +3,9 @@
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'
8
11
  require_relative 'libcall/fiddley'
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.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - kojix2
@@ -47,10 +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
52
54
  - lib/libcall/fiddley.rb
53
55
  - lib/libcall/library_finder.rb
56
+ - lib/libcall/output_reader.rb
54
57
  - lib/libcall/parser.rb
55
58
  - lib/libcall/platform.rb
56
59
  - lib/libcall/type_map.rb
@@ -73,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
73
76
  - !ruby/object:Gem::Version
74
77
  version: '0'
75
78
  requirements: []
76
- rubygems_version: 3.7.2
79
+ rubygems_version: 3.6.9
77
80
  specification_version: 4
78
81
  summary: Call functions in shared libraries directly from the CLI
79
82
  test_files: []