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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39da3018356d7d6e8287d59bb0ca8ccc24f5a8b211d974b51d526d21347ac0f5
4
- data.tar.gz: 6d55af828e7e621226bab1efff15174f00e1c542af390cf8e5886f1bbd8369c1
3
+ metadata.gz: db2f2234f00412d76e307fc12179b0a829953a08bd6aefa83d59bbab866e85f3
4
+ data.tar.gz: bb2cd9b42fb09fc6b6e5201ed98c16dae47258e9a1fb23ef76fc6a49efe17ab3
5
5
  SHA512:
6
- metadata.gz: bda1f14c0ae9d52f1b8edff83a8c949df3742f12e280ba87c562c1dc958507d329b934c3a87a96d3b14975a2751852bc3498e3c4e99fab3963e32888e81c4e57
7
- data.tar.gz: 3621dad8e6ad6d5e3ea3ba52970d87457f4fb3dcb03824f75bd88148424a150ff6e6d4565fe1780c859ffc3787750f78905c8afa4fb24e87341b8a90ab3edd74
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
- # TYPE VALUE pairs
27
- libcall -lm -r f64 sqrt double 16
28
- # => 4.0
26
+ libcall -lm sqrt double 16 -r double # => 4.0
27
+ ```
29
28
 
30
- # Custom library
31
- libcall ./mylib.so add_i32 int 10 int 20 -r i32
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
- # Dry run
85
- libcall --dry-run ./mylib.so test u64 42 -r void
104
+ Dry run
86
105
 
87
- # Using -L and -l (like gcc)
88
- libcall -lmylib -L./build add_i32 int 10 int 20 -r i32
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
- # TYPE/VALUE pairs with -r after function
91
- libcall -lm fabs double -5.5 -r f64
92
- # => 5.5
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
- # Windows: calling C runtime functions
95
- libcall msvcrt.dll sqrt double 16.0 -r f64
96
- # => 4.0
119
+ Windows: accessing environment variables
97
120
 
98
- # Windows: accessing environment variables
121
+ ```powershell
99
122
  libcall msvcrt.dll getenv string "PATH" -r cstr
100
123
  ```
101
124
 
102
125
  ## Type Reference
103
126
 
104
- | Suffix | Type | Range/Note |
105
- | ------ | --------------- | ------------------ |
106
- | `i8` | signed 8-bit | -128 to 127 |
107
- | `u8` | unsigned 8-bit | 0 to 255 |
108
- | `i16` | signed 16-bit | -32768 to 32767 |
109
- | `u16` | unsigned 16-bit | 0 to 65535 |
110
- | `i32` | signed 32-bit | standard int |
111
- | `u32` | unsigned 32-bit | unsigned int |
112
- | `i64` | signed 64-bit | long long |
113
- | `u64` | unsigned 64-bit | unsigned long long |
114
- | `f32` | 32-bit float | single precision |
115
- | `f64` | 64-bit float | double precision |
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
- # void get_version(int* major, int* minor)
137
- libcall -ltest -L ./test/fixtures/libtest/build get_version out:int out:int -r void
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 -ltest -L ./test/fixtures/libtest/build get_version out:int out:int -r void
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:
@@ -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 << Parser.fiddle_type(type_sym)
24
-
25
- if type_sym.is_a?(Array) && type_sym.first == :out
26
- inner = type_sym[1]
27
- ptr = allocate_output_pointer(inner)
28
- out_refs << { index: idx, type: inner, ptr: ptr }
29
- arg_values << ptr.to_i
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 = Parser.fiddle_type(return_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
- value = case ref[:type]
96
- when :char then ref[:ptr][0, Fiddle::SIZEOF_CHAR].unpack1('c')
97
- when :uchar then ref[:ptr][0, Fiddle::SIZEOF_CHAR].unpack1('C')
98
- when :short then ref[:ptr][0, Fiddle::SIZEOF_SHORT].unpack1('s')
99
- when :ushort then ref[:ptr][0, Fiddle::SIZEOF_SHORT].unpack1('S')
100
- when :int then ref[:ptr][0, Fiddle::SIZEOF_INT].unpack1('i')
101
- when :uint then ref[:ptr][0, Fiddle::SIZEOF_INT].unpack1('I')
102
- when :long then ref[:ptr][0, Fiddle::SIZEOF_LONG].unpack1('l!')
103
- when :ulong then ref[:ptr][0, Fiddle::SIZEOF_LONG].unpack1('L!')
104
- when :long_long then ref[:ptr][0, Fiddle::SIZEOF_LONG_LONG].unpack1('q')
105
- when :ulong_long then ref[:ptr][0, Fiddle::SIZEOF_LONG_LONG].unpack1('Q')
106
- when :float then ref[:ptr][0, Fiddle::SIZEOF_FLOAT].unpack1('f')
107
- when :double then ref[:ptr][0, Fiddle::SIZEOF_DOUBLE].unpack1('d')
108
- when :voidp then format('0x%x', ref[:ptr][0, Fiddle::SIZEOF_VOIDP].unpack1('J'))
109
- else
110
- raise Error, "Cannot read output value for type: #{ref[:type]}"
111
- end
112
-
113
- { index: ref[:index], type: ref[:type].to_s, value: value }
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
- Pass arguments as TYPE VALUE pairs only.
92
+ Arguments are passed as TYPE VALUE pairs.
66
93
 
67
94
  Examples:
68
- libcall -lm -r f64 sqrt double 16
69
- libcall -ltest -L ./build add_i32 int 10 int -23 -r int
70
- libcall --dry-run ./mylib.so test u64 42 -r void
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
- if type_sym.is_a?(Array) && type_sym.first == :out
127
- arg_pairs << [type_sym, nil]
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
- else
260
- if result.is_a?(Hash) && result.key?(:outputs)
261
- puts "Result: #{result[:result]}" unless result[:result].nil?
262
- unless result[:outputs].empty?
263
- puts 'Output parameters:'
264
- result[:outputs].each do |out|
265
- puts " [#{out[:index]}] #{out[:type]} = #{out[:value]}"
266
- end
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 lib_name.include?('/') || lib_name.include?('\\')
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 = if PKGConfig.respond_to?(:exist?)
29
- PKGConfig.exist?(lib_name)
30
- else
31
- PKGConfig.respond_to?(:have_package) ? PKGConfig.have_package(lib_name) : false
32
- end
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 = if PKGConfig.respond_to?(:libs_only_L)
36
- PKGConfig.libs_only_L(lib_name).to_s.split.map { |p| p.start_with?('-L') ? p[2..] : p }
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
- paths = []
67
-
68
- if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
69
- # Windows paths
70
- paths << 'C:/Windows/System32'
71
- paths << 'C:/Windows/SysWOW64'
72
-
73
- # MSYS2/MinGW paths
74
- if ENV['MSYSTEM']
75
- msys_prefix = ENV['MINGW_PREFIX'] || 'C:/msys64/mingw64'
76
- paths << "#{msys_prefix}/bin"
77
- paths << "#{msys_prefix}/lib"
78
- end
80
+ (Platform.windows? ? windows_library_paths : unix_library_paths)
81
+ .select { |p| Dir.exist?(p) }
82
+ end
79
83
 
80
- # Add PATH directories on Windows
81
- paths.concat(ENV['PATH'].split(';').map { |p| p.tr('\\', '/') }) if ENV['PATH']
82
- else
83
- # Unix-like systems (Linux, macOS)
84
- # Standard library paths
85
- paths << '/lib'
86
- paths << '/usr/lib'
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
- # macOS paths
99
- if RUBY_PLATFORM =~ /darwin/
100
- paths << '/usr/local/lib'
101
- paths << '/opt/homebrew/lib'
102
- end
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
- # LD_LIBRARY_PATH
105
- paths.concat(ENV['LD_LIBRARY_PATH'].split(':')) if ENV['LD_LIBRARY_PATH']
93
+ # Add PATH directories on Windows
94
+ paths.concat(ENV['PATH'].split(';').map { |p| p.tr('\\', '/') }) if ENV['PATH']
106
95
 
107
- # DYLD_LIBRARY_PATH (macOS)
108
- paths.concat(ENV['DYLD_LIBRARY_PATH'].split(':')) if ENV['DYLD_LIBRARY_PATH']
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
- paths.select { |p| Dir.exist?(p) }
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
- extensions.each do |ext|
134
- name = "#{prefix}#{lib_name}#{ext}"
135
- search_paths.each do |path|
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
- # Check for versioned libraries (libm.so.6, etc.)
140
- next if ext.empty?
137
+ # Check for versioned libraries (libm.so.6, etc.)
138
+ next if ext.empty?
141
139
 
142
- pattern = File.join(path, "#{name}.*")
143
- matches = Dir.glob(pattern).select { |f| File.file?(f) }
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
@@ -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 = TYPE_MAP[inner]
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 = TYPE_MAP[type_str]
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 = TYPE_MAP[type_str]
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.strip_quotes(token)
84
- if (token.start_with?('"') && token.end_with?('"')) || (token.start_with?("'") && token.end_with?("'"))
85
- token[1...-1]
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
- token
119
+ raise Error, "Unknown element type for coercion: #{type_sym}"
88
120
  end
89
121
  end
90
122
 
91
- def self.fiddle_type(type_sym)
92
- # Output parameters are passed as pointers
93
- return Fiddle::TYPE_VOIDP if type_sym.is_a?(Array) && type_sym.first == :out
94
-
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
- raise Error, "Unknown Fiddle type: #{type_sym}"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Libcall
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.3'
5
5
  end
data/lib/libcall.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'libcall/parser'
5
5
  require_relative 'libcall/library_finder'
6
6
  require_relative 'libcall/caller'
7
7
  require_relative 'libcall/cli'
8
+ require_relative 'libcall/fiddley'
8
9
 
9
10
  module Libcall
10
11
  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.1
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.6.9
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: []