libcall 0.0.0 → 0.0.1

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: d33e4ff88ea0b7f5200165a946f7a146cf0a1d4781d6fd5583d4ccdf3ec17dcf
4
- data.tar.gz: 24d91124d8f8207fca38aabadab15cced21106e649755130180d248491e03eb0
3
+ metadata.gz: 39da3018356d7d6e8287d59bb0ca8ccc24f5a8b211d974b51d526d21347ac0f5
4
+ data.tar.gz: 6d55af828e7e621226bab1efff15174f00e1c542af390cf8e5886f1bbd8369c1
5
5
  SHA512:
6
- metadata.gz: b79e721348b381ca177f8662ced2a942f7a34b65102a9dc3e8e852ad726c3501e48fadc311f63b7a86eab3113b2081e20c78de1dbbe4b58f2b6249d8541329fc
7
- data.tar.gz: 9924983519f2f36b5930edab9ae607419eba0199ad6c3aa7a0707ea06006be7512a3d2b1087985e711eeefc928f4d287f23159ba80df240b130acd16cffcfa12
6
+ metadata.gz: bda1f14c0ae9d52f1b8edff83a8c949df3742f12e280ba87c562c1dc958507d329b934c3a87a96d3b14975a2751852bc3498e3c4e99fab3963e32888e81c4e57
7
+ data.tar.gz: 3621dad8e6ad6d5e3ea3ba52970d87457f4fb3dcb03824f75bd88148424a150ff6e6d4565fe1780c859ffc3787750f78905c8afa4fb24e87341b8a90ab3edd74
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # libcall
2
2
 
3
3
  [![test](https://github.com/kojix2/libcall/actions/workflows/main.yml/badge.svg)](https://github.com/kojix2/libcall/actions/workflows/main.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/libcall.svg)](https://badge.fury.io/rb/libcall)
4
5
  [![Lines of Code](https://img.shields.io/endpoint?url=https%3A%2F%2Ftokei.kojix2.net%2Fbadge%2Fgithub%2Fkojix2%2Flibcall%2Flines)](https://tokei.kojix2.net/github/kojix2/libcall)
5
6
 
6
7
  Call C functions in shared libraries from the command line.
@@ -11,54 +12,91 @@ Call C functions in shared libraries from the command line.
11
12
  gem install libcall
12
13
  ```
13
14
 
15
+ **Windows**: Supports DLLs (e.g., `msvcrt.dll`, `kernel32.dll`). Searches in System32, PATH, and MSYS2/MinGW directories. For building custom DLLs, RubyInstaller with DevKit is recommended.
16
+
14
17
  ## Usage
15
18
 
16
19
  ```sh
17
- libcall [OPTIONS] <LIBRARY> <FUNCTION> [ARGS...]
20
+ libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
18
21
  ```
19
22
 
20
23
  ### Quick Examples
21
24
 
22
25
  ```sh
23
- # Math function with type suffix
24
- libcall -lm sqrt 16.0f64 -r f64
26
+ # TYPE VALUE pairs
27
+ libcall -lm -r f64 sqrt double 16
25
28
  # => 4.0
26
29
 
27
30
  # Custom library
28
- libcall ./mylib.so add 10i32 20i32 -r i32
31
+ libcall ./mylib.so add_i32 int 10 int 20 -r i32
29
32
  # => 30
30
33
  ```
31
34
 
32
- ### Type Syntax
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:
33
43
 
34
- Use Rust-style type suffixes:
44
+ - Use `ptr` (or `pointer`) to pass raw addresses as integers
45
+ - Use `null`, `nil`, `NULL`, or `0` to pass a null pointer
46
+
47
+ ```sh
48
+ # Pass a null pointer to a function taking const char*
49
+ libcall -ltest str_length ptr null -r i32
50
+ # => 0
51
+ ```
35
52
 
36
- - Integers: `42i32`, `100u64`, `255u8`
37
- - Floats: `3.14f64`, `2.5f32`
38
- - Strings: `"hello"`
53
+ End of options `--`:
54
+
55
+ - Use `--` to stop option parsing if a value starts with `-`
56
+
57
+ ```sh
58
+ libcall -lc getenv string -- -r -r cstr
59
+ ```
39
60
 
40
61
  ### Options
41
62
 
42
63
  - `-l LIBRARY` - library name (searches standard paths)
43
64
  - `-L PATH` - add library search path
44
65
  - `-r TYPE` - return type (void, i32, f64, cstr, ptr)
66
+ - Options may appear before or after the function name.
45
67
  - `--dry-run` - validate without executing
46
68
  - `--json` - JSON output
47
69
  - `--verbose` - detailed info
48
70
  - `-h, --help` - show help
49
71
  - `-v, --version` - show version
50
72
 
73
+ Library search:
74
+
75
+ - `-L` adds search paths; `-l` resolves by name
76
+ - On Linux and macOS, `LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` are honored
77
+
51
78
  ### More Examples
52
79
 
53
80
  ```sh
54
81
  # JSON output
55
- libcall --json -lm sqrt 9.0f64 -r f64
82
+ libcall --json -lm sqrt double 9.0 -r f64
56
83
 
57
84
  # Dry run
58
- libcall --dry-run ./mylib.so test 42i32 -r void
85
+ libcall --dry-run ./mylib.so test u64 42 -r void
59
86
 
60
87
  # Using -L and -l (like gcc)
61
- libcall -lmylib -L./build add 10i32 20i32 -r i32
88
+ libcall -lmylib -L./build add_i32 int 10 int 20 -r i32
89
+
90
+ # TYPE/VALUE pairs with -r after function
91
+ libcall -lm fabs double -5.5 -r f64
92
+ # => 5.5
93
+
94
+ # Windows: calling C runtime functions
95
+ libcall msvcrt.dll sqrt double 16.0 -r f64
96
+ # => 4.0
97
+
98
+ # Windows: accessing environment variables
99
+ libcall msvcrt.dll getenv string "PATH" -r cstr
62
100
  ```
63
101
 
64
102
  ## Type Reference
@@ -76,12 +114,30 @@ libcall -lmylib -L./build add 10i32 20i32 -r i32
76
114
  | `f32` | 32-bit float | single precision |
77
115
  | `f64` | 64-bit float | double precision |
78
116
 
117
+ Also supported:
118
+
119
+ - `string`: C string argument (char\*)
120
+ - `cstr`: C string return (char\*)
121
+ - `ptr`/`pointer`: void\* pointer
122
+
79
123
  ## pkg-config Support
80
124
 
81
125
  Set `PKG_CONFIG_PATH` and use package names with `-l`:
82
126
 
83
127
  ```sh
84
- PKG_CONFIG_PATH=/path/to/pkgconfig libcall -lmypackage func 42i32 -r i32
128
+ PKG_CONFIG_PATH=/path/to/pkgconfig libcall -lmypackage func i32 42 -r i32
129
+ ```
130
+
131
+ ## Output parameters (out:TYPE)
132
+
133
+ You can pass output pointers by specifying `out:TYPE`. The pointer is allocated automatically, passed to the function, and printed after the call.
134
+
135
+ ```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
138
+
139
+ # JSON includes an "outputs" array
140
+ libcall --json -ltest -L ./test/fixtures/libtest/build get_version out:int out:int -r void
85
141
  ```
86
142
 
87
143
  ## Warning
@@ -3,24 +3,33 @@
3
3
  require 'fiddle'
4
4
 
5
5
  module Libcall
6
+ # Execute C function calls via Fiddle FFI
6
7
  class Caller
7
- attr_reader :lib_path, :func_name, :return_type, :args
8
+ attr_reader :lib_path, :func_name, :return_type, :arg_pairs
8
9
 
9
- def initialize(lib_path, func_name, args: [], return_type: :void)
10
+ def initialize(lib_path, func_name, arg_pairs: [], return_type: :void)
10
11
  @lib_path = lib_path
11
12
  @func_name = func_name
12
13
  @return_type = return_type
13
- @args = args
14
+ @arg_pairs = arg_pairs
14
15
  end
15
16
 
16
17
  def call
17
18
  arg_types = []
18
19
  arg_values = []
20
+ out_refs = []
19
21
 
20
- args.each do |arg|
21
- type_sym, value = Parser.parse_arg(arg)
22
+ arg_pairs.each_with_index do |(type_sym, value), idx|
22
23
  arg_types << Parser.fiddle_type(type_sym)
23
- arg_values << value
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
30
+ else
31
+ arg_values << value
32
+ end
24
33
  end
25
34
 
26
35
  ret_type = Parser.fiddle_type(return_type)
@@ -29,9 +38,14 @@ module Libcall
29
38
  func_ptr = handle[func_name]
30
39
  func = Fiddle::Function.new(func_ptr, arg_types, ret_type)
31
40
 
32
- result = func.call(*arg_values)
41
+ raw_result = func.call(*arg_values)
42
+ formatted_result = format_result(raw_result, return_type)
33
43
 
34
- format_result(result, return_type)
44
+ if out_refs.empty?
45
+ formatted_result
46
+ else
47
+ { result: formatted_result, outputs: read_output_values(out_refs) }
48
+ end
35
49
  rescue Fiddle::DLError => e
36
50
  raise Error, "Failed to load library or function: #{e.message}"
37
51
  end
@@ -59,5 +73,45 @@ module Libcall
59
73
  result
60
74
  end
61
75
  end
76
+
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
+ def read_output_values(out_refs)
94
+ 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 }
114
+ end
115
+ end
62
116
  end
63
117
  end
data/lib/libcall/cli.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'optparse'
4
3
  require 'json'
5
4
 
6
5
  module Libcall
6
+ # Command-line interface for calling C functions from shared libraries
7
7
  class CLI
8
8
  def initialize(argv)
9
9
  @argv = argv
@@ -19,41 +19,31 @@ module Libcall
19
19
  end
20
20
 
21
21
  def run
22
- parse_options!
22
+ lib_path, func_name, arg_pairs = scan_argv!(@argv)
23
23
 
24
- if @argv.empty?
25
- puts @parser.help
26
- exit 1
27
- end
28
-
29
- # Resolve library path
24
+ # Resolve library path if a library name (-l) was given
30
25
  if @options[:lib_name]
31
26
  finder = LibraryFinder.new(lib_paths: @options[:lib_paths])
32
27
  lib_path = finder.find(@options[:lib_name])
33
- else
34
- lib_path = @options[:lib] || @argv.shift
35
28
  end
36
29
 
37
- func_name = @argv.shift
38
- args = @argv
39
-
40
30
  if lib_path.nil? || func_name.nil?
41
31
  warn 'Error: Missing required arguments'
42
- warn 'Usage: libcall <LIBRARY> <FUNCTION> [ARGS...]'
32
+ warn 'Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...'
43
33
  exit 1
44
34
  end
45
35
 
46
36
  if @options[:verbose]
47
37
  warn "Library: #{lib_path}"
48
38
  warn "Function: #{func_name}"
49
- warn "Arguments: #{args.inspect}"
39
+ warn "Arguments: #{arg_pairs.inspect}"
50
40
  warn "Return type: #{@options[:return_type]}"
51
41
  end
52
42
 
53
43
  if @options[:dry_run]
54
- dry_run_info(lib_path, func_name, args)
44
+ dry_run_info(lib_path, func_name, arg_pairs)
55
45
  else
56
- execute_call(lib_path, func_name, args)
46
+ execute_call(lib_path, func_name, arg_pairs)
57
47
  end
58
48
  rescue Error => e
59
49
  warn "Error: #{e.message}"
@@ -66,113 +56,218 @@ module Libcall
66
56
 
67
57
  private
68
58
 
69
- def parse_options!
70
- @parser = OptionParser.new do |opts|
71
- opts.banner = <<~BANNER
72
- Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> [ARGS...]
59
+ def parse_options_banner
60
+ <<~BANNER
61
+ Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
73
62
 
74
- Call C functions in shared libraries from the command line.
63
+ Call C functions in shared libraries from the command line.
75
64
 
76
- Examples:
77
- libcall /lib/libm.so.6 sqrt 16.0f64 -r f64
78
- libcall -lm sqrt 16.0f64 -r f64
79
- libcall -lsum -L. add 10i32 20i32 -r i32
80
- libcall --dry-run ./mylib.so test 42u64 -r void
65
+ Pass arguments as TYPE VALUE pairs only.
81
66
 
82
- Options:
83
- BANNER
67
+ 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
84
71
 
85
- opts.on('--dry-run', 'Validate arguments without executing') do
86
- @options[:dry_run] = true
87
- end
72
+ Options:
73
+ BANNER
74
+ end
88
75
 
89
- opts.on('--json', 'Output result as JSON') do
90
- @options[:json] = true
91
- end
76
+ # Custom scanner that supports:
77
+ # - Known flags anywhere (before/after function name)
78
+ # - Negative numbers as values (not mistaken for options)
79
+ # - TYPE VALUE pairs
80
+ def scan_argv!(argv)
81
+ lib_path = nil
82
+ func_name = nil
83
+ arg_pairs = []
84
+
85
+ positional_only = false
86
+ i = 0
87
+ while i < argv.length
88
+ tok = argv[i]
92
89
 
93
- opts.on('--verbose', 'Show detailed information') do
94
- @options[:verbose] = true
90
+ # End-of-options marker: switch to positional-only mode
91
+ if tok == '--'
92
+ positional_only = true
93
+ i += 1
94
+ next
95
95
  end
96
96
 
97
- opts.on('-l', '--lib LIBRARY', 'Library name (searches in standard paths)') do |lib|
98
- @options[:lib_name] = lib
97
+ # Try to handle as a known option (only if not in positional-only mode)
98
+ unless positional_only
99
+ option_consumed = handle_option!(tok, argv, i)
100
+ if option_consumed > 0
101
+ i += option_consumed
102
+ next
103
+ end
99
104
  end
100
105
 
101
- opts.on('-L', '--lib-path PATH', 'Add library search path') do |path|
102
- @options[:lib_paths] << path
106
+ # Positional resolution for <LIBRARY> and <FUNCTION>
107
+ if lib_path.nil? && @options[:lib_name].nil?
108
+ lib_path = tok
109
+ i += 1
110
+ next
103
111
  end
104
112
 
105
- opts.on('-r', '--ret TYPE', 'Return type (void, i32, f64, cstr, etc.)') do |type|
106
- @options[:return_type] = Parser.parse_return_type(type)
113
+ if func_name.nil?
114
+ func_name = tok
115
+ i += 1
116
+ next
107
117
  end
108
118
 
109
- opts.on('-h', '--help', 'Show help') do
110
- puts opts
111
- exit
119
+ # After function name: parse TYPE VALUE pairs (or TYPE-only for out:TYPE)
120
+ type_tok = tok
121
+ i += 1
122
+
123
+ type_sym = Parser.parse_type(type_tok)
124
+
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]
128
+ next
112
129
  end
113
130
 
114
- opts.on('-v', '--version', 'Show version') do
115
- puts "libcall #{Libcall::VERSION}"
116
- exit
131
+ # Allow `--` between TYPE and VALUE to switch to positional-only
132
+ while i < argv.length && argv[i] == '--'
133
+ positional_only = true
134
+ i += 1
117
135
  end
136
+
137
+ raise Error, "Missing value for argument of type #{type_tok}" if i >= argv.length
138
+
139
+ value_tok = argv[i]
140
+ value = Parser.coerce_value(type_sym, value_tok)
141
+ arg_pairs << [type_sym, value]
142
+ i += 1
118
143
  end
119
144
 
120
- @parser.permute!(@argv)
145
+ [lib_path, func_name, arg_pairs]
121
146
  end
122
147
 
123
- def dry_run_info(lib_path, func_name, args)
124
- info = {
125
- library: lib_path,
126
- function: func_name,
127
- arguments: [],
128
- return_type: @options[:return_type].to_s
129
- }
148
+ # Handle known option flags and return number of consumed tokens (0 if not an option)
149
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
150
+ def handle_option!(tok, argv, i)
151
+ case tok
152
+ when '--dry-run'
153
+ @options[:dry_run] = true
154
+ 1
155
+ when '--json'
156
+ @options[:json] = true
157
+ 1
158
+ when '--verbose'
159
+ @options[:verbose] = true
160
+ 1
161
+ when '-h', '--help'
162
+ puts parse_options_banner
163
+ exit 0
164
+ when '-v', '--version'
165
+ puts "libcall #{Libcall::VERSION}"
166
+ exit 0
167
+ when '-l', '--lib'
168
+ raise Error, 'Missing value for -l/--lib' if i + 1 >= argv.length
130
169
 
131
- args.each_with_index do |arg, i|
132
- type_sym, value = Parser.parse_arg(arg)
133
- info[:arguments] << {
134
- index: i,
135
- raw: arg,
136
- type: type_sym.to_s,
137
- value: value
138
- }
170
+ @options[:lib_name] = argv[i + 1]
171
+ 2
172
+ when /\A-l(.+)\z/
173
+ @options[:lib_name] = ::Regexp.last_match(1)
174
+ 1
175
+ when '-L', '--lib-path'
176
+ raise Error, 'Missing value for -L/--lib-path' if i + 1 >= argv.length
177
+
178
+ @options[:lib_paths] << argv[i + 1]
179
+ 2
180
+ when '-r', '--ret'
181
+ raise Error, 'Missing value for -r/--ret' if i + 1 >= argv.length
182
+
183
+ @options[:return_type] = Parser.parse_return_type(argv[i + 1])
184
+ 2
185
+ when /\A-r(.+)\z/
186
+ @options[:return_type] = Parser.parse_return_type(::Regexp.last_match(1))
187
+ 1
188
+ else
189
+ 0 # Not an option
139
190
  end
191
+ end
192
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
193
+
194
+ def dry_run_info(lib_path, func_name, arg_pairs)
195
+ info = build_info_hash(lib_path, func_name, arg_pairs)
140
196
 
141
197
  if @options[:json]
142
198
  puts JSON.pretty_generate(info)
143
199
  else
144
- puts "Library: #{info[:library]}"
145
- puts "Function: #{info[:function]}"
146
- puts "Return: #{info[:return_type]}"
147
- unless info[:arguments].empty?
148
- puts 'Arguments:'
149
- info[:arguments].each do |arg|
150
- puts " [#{arg[:index]}] #{arg[:raw]} => #{arg[:type]} (#{arg[:value].inspect})"
151
- end
152
- end
200
+ print_info(info)
153
201
  end
154
202
  end
155
203
 
156
- def execute_call(lib_path, func_name, args)
204
+ def build_info_hash(lib_path, func_name, arg_pairs)
205
+ {
206
+ library: lib_path,
207
+ function: func_name,
208
+ arguments: arg_pairs.map.with_index do |(type_sym, value), i|
209
+ {
210
+ index: i,
211
+ type: type_sym.to_s,
212
+ value: value
213
+ }
214
+ end,
215
+ return_type: @options[:return_type].to_s
216
+ }
217
+ end
218
+
219
+ def print_info(info)
220
+ puts "Library: #{info[:library]}"
221
+ puts "Function: #{info[:function]}"
222
+ puts "Return: #{info[:return_type]}"
223
+ return if info[:arguments].empty?
224
+
225
+ puts 'Arguments:'
226
+ info[:arguments].each do |arg|
227
+ puts " [#{arg[:index]}] #{arg[:type]} = #{arg[:value].inspect}"
228
+ end
229
+ end
230
+
231
+ def execute_call(lib_path, func_name, arg_pairs)
157
232
  caller = Caller.new(
158
233
  lib_path,
159
234
  func_name,
160
- args: args,
235
+ arg_pairs: arg_pairs,
161
236
  return_type: @options[:return_type]
162
237
  )
163
238
 
164
239
  result = caller.call
240
+ output_result(lib_path, func_name, result)
241
+ end
165
242
 
243
+ def output_result(lib_path, func_name, result)
166
244
  if @options[:json]
167
245
  output = {
168
246
  library: lib_path,
169
247
  function: func_name,
170
- return_type: @options[:return_type].to_s,
171
- result: result
248
+ return_type: @options[:return_type].to_s
172
249
  }
250
+
251
+ if result.is_a?(Hash) && result.key?(:outputs)
252
+ output[:result] = result[:result]
253
+ output[:outputs] = result[:outputs]
254
+ else
255
+ output[:result] = result
256
+ end
257
+
173
258
  puts JSON.pretty_generate(output, allow_nan: true)
174
259
  else
175
- puts result unless result.nil?
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
267
+ end
268
+ else
269
+ puts result unless result.nil?
270
+ end
176
271
  end
177
272
  end
178
273
  end
@@ -9,6 +9,7 @@ rescue LoadError
9
9
  end
10
10
 
11
11
  module Libcall
12
+ # Find shared libraries by name using standard search paths and pkg-config
12
13
  class LibraryFinder
13
14
  def initialize(lib_paths: [])
14
15
  @lib_paths = lib_paths
@@ -64,31 +65,48 @@ module Libcall
64
65
  def default_library_paths
65
66
  paths = []
66
67
 
67
- # Standard library paths
68
- paths << '/lib'
69
- paths << '/usr/lib'
70
- paths << '/usr/local/lib'
71
-
72
- # Architecture-specific paths
73
- if RUBY_PLATFORM =~ /x86_64/
74
- paths << '/lib/x86_64-linux-gnu'
75
- paths << '/usr/lib/x86_64-linux-gnu'
76
- elsif RUBY_PLATFORM =~ /aarch64|arm64/
77
- paths << '/lib/aarch64-linux-gnu'
78
- paths << '/usr/lib/aarch64-linux-gnu'
79
- end
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
79
 
81
- # macOS paths
82
- if RUBY_PLATFORM =~ /darwin/
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'
83
87
  paths << '/usr/local/lib'
84
- paths << '/opt/homebrew/lib'
85
- end
86
88
 
87
- # LD_LIBRARY_PATH
88
- paths.concat(ENV['LD_LIBRARY_PATH'].split(':')) if ENV['LD_LIBRARY_PATH']
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
89
97
 
90
- # DYLD_LIBRARY_PATH (macOS)
91
- paths.concat(ENV['DYLD_LIBRARY_PATH'].split(':')) if ENV['DYLD_LIBRARY_PATH']
98
+ # macOS paths
99
+ if RUBY_PLATFORM =~ /darwin/
100
+ paths << '/usr/local/lib'
101
+ paths << '/opt/homebrew/lib'
102
+ end
103
+
104
+ # LD_LIBRARY_PATH
105
+ paths.concat(ENV['LD_LIBRARY_PATH'].split(':')) if ENV['LD_LIBRARY_PATH']
106
+
107
+ # DYLD_LIBRARY_PATH (macOS)
108
+ paths.concat(ENV['DYLD_LIBRARY_PATH'].split(':')) if ENV['DYLD_LIBRARY_PATH']
109
+ end
92
110
 
93
111
  paths.select { |p| Dir.exist?(p) }
94
112
  end
@@ -101,8 +119,15 @@ module Libcall
101
119
  end
102
120
 
103
121
  # Try with lib prefix and common extensions
104
- extensions = ['', '.so', '.dylib', '.dll']
105
- prefixes = lib_name.start_with?('lib') ? [''] : ['lib']
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
+ prefixes = lib_name.start_with?('lib') ? [''] : ['lib', '']
106
131
 
107
132
  prefixes.each do |prefix|
108
133
  extensions.each do |ext|
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Libcall
4
+ # Parse and coerce TYPE VALUE argument pairs for FFI calls
4
5
  class Parser
5
6
  TYPE_MAP = {
6
7
  'i8' => :char,
@@ -17,7 +18,9 @@ module Libcall
17
18
  'f64' => :double,
18
19
  'cstr' => :string,
19
20
  'ptr' => :voidp,
21
+ 'pointer' => :voidp,
20
22
  'void' => :void,
23
+ # Common aliases
21
24
  'int' => :int,
22
25
  'uint' => :uint,
23
26
  'long' => :long,
@@ -25,40 +28,27 @@ module Libcall
25
28
  'float' => :float,
26
29
  'double' => :double,
27
30
  'char' => :char,
28
- 'str' => :string
31
+ 'str' => :string,
32
+ 'string' => :string
29
33
  }.freeze
30
34
 
31
- def self.parse_arg(arg)
32
- return [:string, ''] if arg.empty?
35
+ INTEGER_TYPES = %i[int uint long ulong long_long ulong_long char uchar short ushort].freeze
36
+ FLOAT_TYPES = %i[float double].freeze
33
37
 
34
- if (arg.start_with?('"') && arg.end_with?('"')) ||
35
- (arg.start_with?("'") && arg.end_with?("'"))
36
- return [:string, arg[1...-1]]
38
+ # Pair-only API helpers
39
+ def self.parse_type(type_str)
40
+ # Output pointer spec: out:TYPE (e.g., out:int, out:f64)
41
+ if type_str.start_with?('out:')
42
+ inner = type_str.sub(/^out:/, '')
43
+ inner_sym = TYPE_MAP[inner]
44
+ raise Error, "Unknown type in out: #{inner}" unless inner_sym
45
+ return [:out, inner_sym]
37
46
  end
38
47
 
39
- if arg =~ /^([-+]?(?:\d+\.?\d*|\d*\.\d+))([a-z]\d+|[a-z]+)$/i
40
- value_str = ::Regexp.last_match(1)
41
- type_str = ::Regexp.last_match(2)
42
-
43
- type_sym = TYPE_MAP[type_str]
44
- raise Error, "Unknown type suffix: #{type_str}" unless type_sym
45
-
46
- value = if %i[float double].include?(type_sym)
47
- value_str.to_f
48
- else
49
- value_str.to_i
50
- end
51
-
52
- return [type_sym, value]
53
- end
48
+ type_sym = TYPE_MAP[type_str]
49
+ raise Error, "Unknown type: #{type_str}" unless type_sym
54
50
 
55
- if arg =~ /^[-+]?\d+$/
56
- [:int, arg.to_i]
57
- elsif arg =~ /^[-+]?(?:\d+\.\d*|\d*\.\d+)$/
58
- [:double, arg.to_f]
59
- else
60
- raise Error, "Cannot parse argument: #{arg}"
61
- end
51
+ type_sym
62
52
  end
63
53
 
64
54
  def self.parse_return_type(type_str)
@@ -70,17 +60,38 @@ module Libcall
70
60
  type_sym
71
61
  end
72
62
 
73
- def self.parse_signature(sig)
74
- raise Error, "Invalid signature format: #{sig}" unless sig =~ /^([a-z]\w*)\((.*)\)$/i
63
+ def self.coerce_value(type_sym, token)
64
+ case type_sym
65
+ when *FLOAT_TYPES
66
+ Float(token)
67
+ when *INTEGER_TYPES
68
+ Integer(token)
69
+ when :voidp
70
+ # Accept common null tokens for pointer types
71
+ return 0 if token =~ /\A(null|nil|NULL|0)\z/
75
72
 
76
- ret_type = parse_return_type(::Regexp.last_match(1))
77
- arg_types = ::Regexp.last_match(2).split(',').map(&:strip).reject(&:empty?).map do |t|
78
- TYPE_MAP[t] or raise Error, "Unknown type in signature: #{t}"
73
+ Integer(token)
74
+ when :string
75
+ strip_quotes(token)
76
+ when :void
77
+ raise Error, 'void cannot be used as an argument type'
78
+ else
79
+ raise Error, "Unknown type for coercion: #{type_sym}"
80
+ end
81
+ end
82
+
83
+ def self.strip_quotes(token)
84
+ if (token.start_with?('"') && token.end_with?('"')) || (token.start_with?("'") && token.end_with?("'"))
85
+ token[1...-1]
86
+ else
87
+ token
79
88
  end
80
- [ret_type, arg_types]
81
89
  end
82
90
 
83
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
+
84
95
  case type_sym
85
96
  when :void then Fiddle::TYPE_VOID
86
97
  when :char then Fiddle::TYPE_CHAR
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Libcall
4
- VERSION = '0.0.0'
4
+ VERSION = '0.0.1'
5
5
  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.0
4
+ version: 0.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kojix2