libcall 0.0.0 → 0.0.2

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: b45f825f6989bd4673b982435ce1bad8ff1b1785785118ffa66c8bdad84b1d7c
4
+ data.tar.gz: 9a60c1d6d69b2ff7e70a2884b6dfbc8e2b6099c87df40558559fdffffff5c65c
5
5
  SHA512:
6
- metadata.gz: b79e721348b381ca177f8662ced2a942f7a34b65102a9dc3e8e852ad726c3501e48fadc311f63b7a86eab3113b2081e20c78de1dbbe4b58f2b6249d8541329fc
7
- data.tar.gz: 9924983519f2f36b5930edab9ae607419eba0199ad6c3aa7a0707ea06006be7512a3d2b1087985e711eeefc928f4d287f23159ba80df240b130acd16cffcfa12
6
+ metadata.gz: ed0cde569e1eea83480b744907d3397f6e7abae68b991993090312edc6ccf74d4cf973afbad2dd3402142eccf390a3f992cf2174ad016ea0009d5d8b91e104bd
7
+ data.tar.gz: 42571c69fd4e1487e17b592ec98afc321ed646e938d9d773a96c4e450f4762c9d49975f7de47a33ba5f747e2e2c1367da00d7a3233ff059a8ab5a70f43a3bb92
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,31 +12,51 @@ 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
- # Custom library
28
- libcall ./mylib.so add 10i32 20i32 -r i32
29
- # => 30
30
+ # libc strlen
31
+ libcall -lc strlen string "hello" -r usize
32
+ # => 5
33
+ ```
34
+
35
+ ### Argument Syntax
36
+
37
+ Pass arguments as TYPE VALUE pairs (single-token suffix style has been removed):
38
+
39
+ - Examples: `int 10`, `double -3.14`, `string "hello"`
40
+ - Negative values are safe (not treated as options): `int -23`
41
+
42
+ Pointers and null:
43
+
44
+ - Use `ptr` (or `pointer`) to pass raw addresses as integers
45
+ - Use `null`, `nil`, `NULL`, or `0` to pass a null pointer
46
+
47
+ ```sh
48
+ # Pass a null pointer to a function taking const char*
49
+ libcall -ltest str_length ptr null -r i32
50
+ # => 0
30
51
  ```
31
52
 
32
- ### Type Syntax
53
+ End of options `--`:
33
54
 
34
- Use Rust-style type suffixes:
55
+ - Use `--` to stop option parsing if a value starts with `-`
35
56
 
36
- - Integers: `42i32`, `100u64`, `255u8`
37
- - Floats: `3.14f64`, `2.5f32`
38
- - Strings: `"hello"`
57
+ ```sh
58
+ libcall -lc getenv string -- -r -r cstr
59
+ ```
39
60
 
40
61
  ### Options
41
62
 
@@ -48,40 +69,95 @@ Use Rust-style type suffixes:
48
69
  - `-h, --help` - show help
49
70
  - `-v, --version` - show version
50
71
 
72
+ Library search:
73
+
74
+ - `-L` adds search paths; `-l` resolves by name
75
+ - On Linux and macOS, `LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` are honored
76
+
51
77
  ### More Examples
52
78
 
53
79
  ```sh
54
80
  # JSON output
55
- libcall --json -lm sqrt 9.0f64 -r f64
81
+ libcall --json -lm sqrt double 9.0 -r f64
56
82
 
57
83
  # Dry run
58
- libcall --dry-run ./mylib.so test 42i32 -r void
84
+ libcall --dry-run -lc getpid -r int
85
+
86
+ # Output parameter with libm
87
+ libcall -lm modf double -3.14 out:double -r f64
59
88
 
60
- # Using -L and -l (like gcc)
61
- libcall -lmylib -L./build add 10i32 20i32 -r i32
89
+ # TYPE/VALUE pairs with -r after function
90
+ libcall -lm fabs double -5.5 -r f64
91
+ # => 5.5
92
+
93
+ # Windows: calling C runtime functions
94
+ libcall msvcrt.dll sqrt double 16.0 -r f64
95
+ # => 4.0
96
+
97
+ # Windows: accessing environment variables
98
+ libcall msvcrt.dll getenv string "PATH" -r cstr
62
99
  ```
63
100
 
64
101
  ## Type Reference
65
102
 
66
- | Suffix | Type | Range/Note |
67
- | ------ | --------------- | ------------------ |
68
- | `i8` | signed 8-bit | -128 to 127 |
69
- | `u8` | unsigned 8-bit | 0 to 255 |
70
- | `i16` | signed 16-bit | -32768 to 32767 |
71
- | `u16` | unsigned 16-bit | 0 to 65535 |
72
- | `i32` | signed 32-bit | standard int |
73
- | `u32` | unsigned 32-bit | unsigned int |
74
- | `i64` | signed 64-bit | long long |
75
- | `u64` | unsigned 64-bit | unsigned long long |
76
- | `f32` | 32-bit float | single precision |
77
- | `f64` | 64-bit float | double precision |
103
+ | Short (suffix) | Formal (C) | Note/Range |
104
+ | -------------- | ----------------------------------- | ------------------ |
105
+ | `i8` | `char` (≈ `int8_t`) | -128 to 127 |
106
+ | `u8` | `unsigned char` (≈ `uint8_t`) | 0 to 255 |
107
+ | `i16` | `short` (≈ `int16_t`) | -32768 to 32767 |
108
+ | `u16` | `unsigned short` (≈ `uint16_t`) | 0 to 65535 |
109
+ | `i32` | `int` (≈ `int32_t`) | typical 32-bit int |
110
+ | `u32` | `unsigned int` (≈ `uint32_t`) | unsigned 32-bit |
111
+ | `i64` | `long long` (≈ `int64_t`) | 64-bit |
112
+ | `u64` | `unsigned long long` (≈ `uint64_t`) | 64-bit |
113
+ | `f32` | `float` | single precision |
114
+ | `f64` | `double` | double precision |
115
+
116
+ Also supported:
117
+
118
+ - `string`: C string argument (char\*)
119
+ - `cstr`: C string return (char\*)
120
+ - `ptr`/`pointer`: void\* pointer
78
121
 
79
122
  ## pkg-config Support
80
123
 
81
124
  Set `PKG_CONFIG_PATH` and use package names with `-l`:
82
125
 
83
126
  ```sh
84
- PKG_CONFIG_PATH=/path/to/pkgconfig libcall -lmypackage func 42i32 -r i32
127
+ PKG_CONFIG_PATH=/path/to/pkgconfig libcall -lmypackage func i32 42 -r i32
128
+ ```
129
+
130
+ ## Output parameters (out:TYPE)
131
+
132
+ You can pass output pointers by specifying `out:TYPE`. The pointer is allocated automatically, passed to the function, and printed after the call.
133
+
134
+ ```sh
135
+ # double frexp(double x, int* exp)
136
+ libcall -lm frexp double 8.0 out:int -r f64
137
+
138
+ # JSON includes an "outputs" array
139
+ libcall --json -lm frexp double 8.0 out:int -r f64
140
+ ```
141
+
142
+ ## Arrays
143
+
144
+ - Input arrays: `TYPE[]` takes a comma-separated value list.
145
+
146
+ ```sh
147
+ # zlib (Linux/macOS): uLong crc32(uLong crc, const Bytef* buf, uInt len)
148
+ libcall -lz crc32 uint 0 uchar[] 104,101,108,108,111 uint 5 -r uint
149
+ ```
150
+
151
+ - Output arrays: `out:TYPE[N]` allocates N elements and prints them after the call.
152
+
153
+ ```sh
154
+ # Linux (libc): ssize_t getrandom(void* buf, size_t buflen, unsigned int flags)
155
+ libcall -lc getrandom out:uchar[16] size_t 16 uint 0 -r long
156
+ ```
157
+
158
+ ```sh
159
+ # macOS (libSystem): void arc4random_buf(void* buf, size_t nbytes)
160
+ libcall -lSystem arc4random_buf out:uchar[16] size_t 16 -r void
85
161
  ```
86
162
 
87
163
  ## Warning
@@ -3,35 +3,66 @@
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_types << Parser.fiddle_type(type_sym)
23
- arg_values << value
22
+ arg_pairs.each_with_index do |(type_sym, value), idx|
23
+ arg_types << TypeMap.to_fiddle_type(type_sym)
24
+
25
+ if type_sym.is_a?(Array)
26
+ case type_sym.first
27
+ when :out
28
+ inner = type_sym[1]
29
+ ptr = TypeMap.allocate_output_pointer(inner)
30
+ out_refs << { index: idx, kind: :out, type: inner, ptr: ptr }
31
+ arg_values << ptr.to_i
32
+ when :array
33
+ base = type_sym[1]
34
+ values = Array(value)
35
+ ptr = TypeMap.allocate_array(base, values.length)
36
+ TypeMap.write_array(ptr, base, values)
37
+ arg_values << ptr.to_i
38
+ when :out_array
39
+ base = type_sym[1]
40
+ count = type_sym[2]
41
+ ptr = TypeMap.allocate_array(base, count)
42
+ out_refs << { index: idx, kind: :out_array, base: base, count: count, ptr: ptr }
43
+ arg_values << ptr.to_i
44
+ else
45
+ raise Error, "Unknown array/output form: #{type_sym.inspect}"
46
+ end
47
+ else
48
+ arg_values << value
49
+ end
24
50
  end
25
51
 
26
- ret_type = Parser.fiddle_type(return_type)
52
+ ret_type = TypeMap.to_fiddle_type(return_type)
27
53
 
28
54
  handle = Fiddle.dlopen(lib_path)
29
55
  func_ptr = handle[func_name]
30
56
  func = Fiddle::Function.new(func_ptr, arg_types, ret_type)
31
57
 
32
- result = func.call(*arg_values)
58
+ raw_result = func.call(*arg_values)
59
+ formatted_result = format_result(raw_result, return_type)
33
60
 
34
- format_result(result, return_type)
61
+ if out_refs.empty?
62
+ formatted_result
63
+ else
64
+ { result: formatted_result, outputs: read_output_values(out_refs) }
65
+ end
35
66
  rescue Fiddle::DLError => e
36
67
  raise Error, "Failed to load library or function: #{e.message}"
37
68
  end
@@ -59,5 +90,22 @@ module Libcall
59
90
  result
60
91
  end
61
92
  end
93
+
94
+ def read_output_values(out_refs)
95
+ out_refs.map do |ref|
96
+ case ref[:kind]
97
+ when :out
98
+ value = TypeMap.read_output_pointer(ref[:ptr], ref[:type])
99
+ { index: ref[:index], type: ref[:type].to_s, value: value }
100
+ when :out_array
101
+ base = ref[:base]
102
+ count = ref[:count]
103
+ values = TypeMap.read_array(ref[:ptr], base, count)
104
+ { index: ref[:index], type: "#{base}[#{count}]", value: values }
105
+ else
106
+ raise Error, "Unknown out reference kind: #{ref[:kind]}"
107
+ end
108
+ end
109
+ end
62
110
  end
63
111
  end
data/lib/libcall/cli.rb CHANGED
@@ -1,10 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'optparse'
4
3
  require 'json'
4
+ require_relative 'platform'
5
5
 
6
6
  module Libcall
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 = {
@@ -19,41 +38,31 @@ module Libcall
19
38
  end
20
39
 
21
40
  def run
22
- parse_options!
23
-
24
- if @argv.empty?
25
- puts @parser.help
26
- exit 1
27
- end
41
+ lib_path, func_name, arg_pairs = scan_argv!(@argv)
28
42
 
29
- # Resolve library path
43
+ # Resolve library path if a library name (-l) was given
30
44
  if @options[:lib_name]
31
45
  finder = LibraryFinder.new(lib_paths: @options[:lib_paths])
32
46
  lib_path = finder.find(@options[:lib_name])
33
- else
34
- lib_path = @options[:lib] || @argv.shift
35
47
  end
36
48
 
37
- func_name = @argv.shift
38
- args = @argv
39
-
40
49
  if lib_path.nil? || func_name.nil?
41
50
  warn 'Error: Missing required arguments'
42
- warn 'Usage: libcall <LIBRARY> <FUNCTION> [ARGS...]'
51
+ warn 'Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...'
43
52
  exit 1
44
53
  end
45
54
 
46
55
  if @options[:verbose]
47
56
  warn "Library: #{lib_path}"
48
57
  warn "Function: #{func_name}"
49
- warn "Arguments: #{args.inspect}"
58
+ warn "Arguments: #{arg_pairs.inspect}"
50
59
  warn "Return type: #{@options[:return_type]}"
51
60
  end
52
61
 
53
62
  if @options[:dry_run]
54
- dry_run_info(lib_path, func_name, args)
63
+ dry_run_info(lib_path, func_name, arg_pairs)
55
64
  else
56
- execute_call(lib_path, func_name, args)
65
+ execute_call(lib_path, func_name, arg_pairs)
57
66
  end
58
67
  rescue Error => e
59
68
  warn "Error: #{e.message}"
@@ -66,111 +75,228 @@ module Libcall
66
75
 
67
76
  private
68
77
 
69
- def parse_options!
70
- @parser = OptionParser.new do |opts|
71
- opts.banner = <<~BANNER
72
- Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> [ARGS...]
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
73
86
 
74
- Call C functions in shared libraries from the command line.
87
+ <<~BANNER
88
+ Usage: libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
75
89
 
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
90
+ Call C functions in shared libraries from the command line.
81
91
 
82
- Options:
83
- BANNER
92
+ Arguments are passed as TYPE VALUE pairs.
84
93
 
85
- opts.on('--dry-run', 'Validate arguments without executing') do
86
- @options[:dry_run] = true
87
- end
94
+ Examples:
95
+ #{examples.lines.map { |line| line.chomp }.join("\n ")}
88
96
 
89
- opts.on('--json', 'Output result as JSON') do
90
- @options[:json] = true
91
- end
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
106
+ BANNER
107
+ end
108
+
109
+ # Custom scanner that supports:
110
+ # - Known flags anywhere (before/after function name)
111
+ # - Negative numbers as values (not mistaken for options)
112
+ # - TYPE VALUE pairs
113
+ def scan_argv!(argv)
114
+ lib_path = nil
115
+ func_name = nil
116
+ arg_pairs = []
117
+
118
+ positional_only = false
119
+ i = 0
120
+ while i < argv.length
121
+ tok = argv[i]
92
122
 
93
- opts.on('--verbose', 'Show detailed information') do
94
- @options[:verbose] = true
123
+ # End-of-options marker: switch to positional-only mode
124
+ if tok == '--'
125
+ positional_only = true
126
+ i += 1
127
+ next
95
128
  end
96
129
 
97
- opts.on('-l', '--lib LIBRARY', 'Library name (searches in standard paths)') do |lib|
98
- @options[:lib_name] = lib
130
+ # Try to handle as a known option (only if not in positional-only mode)
131
+ unless positional_only
132
+ option_consumed = handle_option!(tok, argv, i)
133
+ if option_consumed > 0
134
+ i += option_consumed
135
+ next
136
+ end
99
137
  end
100
138
 
101
- opts.on('-L', '--lib-path PATH', 'Add library search path') do |path|
102
- @options[:lib_paths] << path
139
+ # Positional resolution for <LIBRARY> and <FUNCTION>
140
+ if lib_path.nil? && @options[:lib_name].nil?
141
+ lib_path = tok
142
+ i += 1
143
+ next
103
144
  end
104
145
 
105
- opts.on('-r', '--ret TYPE', 'Return type (void, i32, f64, cstr, etc.)') do |type|
106
- @options[:return_type] = Parser.parse_return_type(type)
146
+ if func_name.nil?
147
+ func_name = tok
148
+ i += 1
149
+ next
107
150
  end
108
151
 
109
- opts.on('-h', '--help', 'Show help') do
110
- puts opts
111
- exit
152
+ # After function name: parse TYPE VALUE pairs (or TYPE-only for out:TYPE)
153
+ type_tok = tok
154
+ i += 1
155
+
156
+ type_sym = Parser.parse_type(type_tok)
157
+
158
+ # TYPE that represents an output pointer/array does not require a value
159
+ if type_sym.is_a?(Array) && %i[out out_array].include?(type_sym.first)
160
+ arg_pairs << [type_sym, nil]
161
+ next
112
162
  end
113
163
 
114
- opts.on('-v', '--version', 'Show version') do
115
- puts "libcall #{Libcall::VERSION}"
116
- exit
164
+ # Allow `--` between TYPE and VALUE to switch to positional-only
165
+ while i < argv.length && argv[i] == '--'
166
+ positional_only = true
167
+ i += 1
117
168
  end
169
+
170
+ raise Error, "Missing value for argument of type #{type_tok}" if i >= argv.length
171
+
172
+ value_tok = argv[i]
173
+ value = Parser.coerce_value(type_sym, value_tok)
174
+ arg_pairs << [type_sym, value]
175
+ i += 1
118
176
  end
119
177
 
120
- @parser.permute!(@argv)
178
+ [lib_path, func_name, arg_pairs]
121
179
  end
122
180
 
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
- }
181
+ # Handle known option flags and return number of consumed tokens (0 if not an option)
182
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
183
+ def handle_option!(tok, argv, i)
184
+ case tok
185
+ when '--dry-run'
186
+ @options[:dry_run] = true
187
+ 1
188
+ when '--json'
189
+ @options[:json] = true
190
+ 1
191
+ when '--verbose'
192
+ @options[:verbose] = true
193
+ 1
194
+ when '-h', '--help'
195
+ puts parse_options_banner
196
+ exit 0
197
+ when '-v', '--version'
198
+ puts "libcall #{Libcall::VERSION}"
199
+ exit 0
200
+ when '-l', '--lib'
201
+ raise Error, 'Missing value for -l/--lib' if i + 1 >= argv.length
130
202
 
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
- }
203
+ @options[:lib_name] = argv[i + 1]
204
+ 2
205
+ when /\A-l(.+)\z/
206
+ @options[:lib_name] = ::Regexp.last_match(1)
207
+ 1
208
+ when '-L', '--lib-path'
209
+ raise Error, 'Missing value for -L/--lib-path' if i + 1 >= argv.length
210
+
211
+ @options[:lib_paths] << argv[i + 1]
212
+ 2
213
+ when '-r', '--ret'
214
+ raise Error, 'Missing value for -r/--ret' if i + 1 >= argv.length
215
+
216
+ @options[:return_type] = Parser.parse_return_type(argv[i + 1])
217
+ 2
218
+ when /\A-r(.+)\z/
219
+ @options[:return_type] = Parser.parse_return_type(::Regexp.last_match(1))
220
+ 1
221
+ else
222
+ 0 # Not an option
139
223
  end
224
+ end
225
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/AbcSize
226
+
227
+ def dry_run_info(lib_path, func_name, arg_pairs)
228
+ info = build_info_hash(lib_path, func_name, arg_pairs)
140
229
 
141
230
  if @options[:json]
142
231
  puts JSON.pretty_generate(info)
143
232
  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
233
+ print_info(info)
234
+ end
235
+ end
236
+
237
+ def build_info_hash(lib_path, func_name, arg_pairs)
238
+ {
239
+ library: lib_path,
240
+ function: func_name,
241
+ arguments: arg_pairs.map.with_index do |(type_sym, value), i|
242
+ {
243
+ index: i,
244
+ type: type_sym.to_s,
245
+ value: value
246
+ }
247
+ end,
248
+ return_type: @options[:return_type].to_s
249
+ }
250
+ end
251
+
252
+ def print_info(info)
253
+ puts "Library: #{info[:library]}"
254
+ puts "Function: #{info[:function]}"
255
+ puts "Return: #{info[:return_type]}"
256
+ return if info[:arguments].empty?
257
+
258
+ puts 'Arguments:'
259
+ info[:arguments].each do |arg|
260
+ puts " [#{arg[:index]}] #{arg[:type]} = #{arg[:value].inspect}"
153
261
  end
154
262
  end
155
263
 
156
- def execute_call(lib_path, func_name, args)
264
+ def execute_call(lib_path, func_name, arg_pairs)
157
265
  caller = Caller.new(
158
266
  lib_path,
159
267
  func_name,
160
- args: args,
268
+ arg_pairs: arg_pairs,
161
269
  return_type: @options[:return_type]
162
270
  )
163
271
 
164
272
  result = caller.call
273
+ output_result(lib_path, func_name, result)
274
+ end
165
275
 
276
+ def output_result(lib_path, func_name, result)
166
277
  if @options[:json]
167
278
  output = {
168
279
  library: lib_path,
169
280
  function: func_name,
170
- return_type: @options[:return_type].to_s,
171
- result: result
281
+ return_type: @options[:return_type].to_s
172
282
  }
283
+
284
+ if result.is_a?(Hash) && result.key?(:outputs)
285
+ output[:result] = result[:result]
286
+ output[:outputs] = result[:outputs]
287
+ else
288
+ output[:result] = result
289
+ end
290
+
173
291
  puts JSON.pretty_generate(output, allow_nan: true)
292
+ elsif result.is_a?(Hash) && result.key?(:outputs)
293
+ puts "Result: #{result[:result]}" unless result[:result].nil?
294
+ unless result[:outputs].empty?
295
+ puts 'Output parameters:'
296
+ result[:outputs].each do |out|
297
+ puts " [#{out[:index]}] #{out[:type]} = #{out[:value]}"
298
+ end
299
+ end
174
300
  else
175
301
  puts result unless result.nil?
176
302
  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'
@@ -9,6 +10,7 @@ rescue LoadError
9
10
  end
10
11
 
11
12
  module Libcall
13
+ # Find shared libraries by name using standard search paths and pkg-config
12
14
  class LibraryFinder
13
15
  def initialize(lib_paths: [])
14
16
  @lib_paths = lib_paths
@@ -18,29 +20,23 @@ module Libcall
18
20
  # Find library by name (e.g., "m" -> "/lib/x86_64-linux-gnu/libm.so.6")
19
21
  def find(lib_name)
20
22
  # If it's a path, return as-is
21
- return File.expand_path(lib_name) if lib_name.include?('/') || lib_name.include?('\\')
22
- return File.expand_path(lib_name) if File.file?(lib_name)
23
+ return File.expand_path(lib_name) if path_like?(lib_name)
23
24
 
24
25
  search_paths = @lib_paths + @default_paths
25
26
 
26
27
  if defined?(PKGConfig)
27
- pkg_exists = if PKGConfig.respond_to?(:exist?)
28
- PKGConfig.exist?(lib_name)
29
- else
30
- PKGConfig.respond_to?(:have_package) ? PKGConfig.have_package(lib_name) : false
31
- 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
32
36
 
33
37
  if pkg_exists
34
- lib_dirs = if PKGConfig.respond_to?(:libs_only_L)
35
- PKGConfig.libs_only_L(lib_name).to_s.split.map { |p| p.start_with?('-L') ? p[2..] : p }
36
- else
37
- PKGConfig.libs(lib_name).to_s.split.select { |t| t.start_with?('-L') }.map { |t| t[2..] }
38
- end
39
- lib_names = if PKGConfig.respond_to?(:libs_only_l)
40
- PKGConfig.libs_only_l(lib_name).to_s.split.map { |l| l.start_with?('-l') ? l[2..] : l }
41
- else
42
- PKGConfig.libs(lib_name).to_s.split.select { |t| t.start_with?('-l') }.map { |t| t[2..] }
43
- end
38
+ lib_dirs = extract_pkg_config_flags(lib_name, 'L')
39
+ lib_names = extract_pkg_config_flags(lib_name, 'l')
44
40
 
45
41
  search_paths = lib_dirs + search_paths
46
42
 
@@ -61,36 +57,65 @@ module Libcall
61
57
 
62
58
  private
63
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
+
64
79
  def default_library_paths
65
- paths = []
80
+ (Platform.windows? ? windows_library_paths : unix_library_paths)
81
+ .select { |p| Dir.exist?(p) }
82
+ end
66
83
 
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'
84
+ def windows_library_paths
85
+ paths = %w[C:/Windows/System32 C:/Windows/SysWOW64]
86
+
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"])
79
91
  end
80
92
 
81
- # macOS paths
82
- if RUBY_PLATFORM =~ /darwin/
83
- paths << '/usr/local/lib'
84
- paths << '/opt/homebrew/lib'
93
+ # Add PATH directories on Windows
94
+ paths.concat(ENV['PATH'].split(';').map { |p| p.tr('\\', '/') }) if ENV['PATH']
95
+
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])
85
109
  end
86
110
 
87
- # LD_LIBRARY_PATH
88
- paths.concat(ENV['LD_LIBRARY_PATH'].split(':')) if ENV['LD_LIBRARY_PATH']
111
+ # macOS paths
112
+ paths.concat(%w[/usr/local/lib /opt/homebrew/lib]) if Platform.darwin?
89
113
 
90
- # DYLD_LIBRARY_PATH (macOS)
91
- paths.concat(ENV['DYLD_LIBRARY_PATH'].split(':')) if ENV['DYLD_LIBRARY_PATH']
114
+ # Environment-based paths
115
+ paths.concat(ENV.fetch('LD_LIBRARY_PATH', '').split(':'))
116
+ paths.concat(ENV.fetch('DYLD_LIBRARY_PATH', '').split(':'))
92
117
 
93
- paths.select { |p| Dir.exist?(p) }
118
+ paths
94
119
  end
95
120
 
96
121
  def resolve_by_name_in_paths(lib_name, search_paths)
@@ -101,24 +126,19 @@ module Libcall
101
126
  end
102
127
 
103
128
  # Try with lib prefix and common extensions
104
- extensions = ['', '.so', '.dylib', '.dll']
105
- prefixes = lib_name.start_with?('lib') ? [''] : ['lib']
106
-
107
- prefixes.each do |prefix|
108
- extensions.each do |ext|
109
- name = "#{prefix}#{lib_name}#{ext}"
110
- search_paths.each do |path|
111
- full_path = File.join(path, name)
112
- return File.expand_path(full_path) if File.file?(full_path)
113
-
114
- # Check for versioned libraries (libm.so.6, etc.)
115
- next if ext.empty?
116
-
117
- pattern = File.join(path, "#{name}.*")
118
- matches = Dir.glob(pattern).select { |f| File.file?(f) }
119
- return File.expand_path(matches.first) unless matches.empty?
120
- end
121
- end
129
+ prefixes = lib_name.start_with?('lib') ? [''] : ['lib', '']
130
+ extensions = Platform.library_extensions
131
+
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)
136
+
137
+ # Check for versioned libraries (libm.so.6, etc.)
138
+ next if ext.empty?
139
+
140
+ matches = Dir.glob(File.join(path, "#{name}.*")).select { |f| File.file?(f) }
141
+ return File.expand_path(matches.first) unless matches.empty?
122
142
  end
123
143
 
124
144
  nil
@@ -1,100 +1,101 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'type_map'
4
+
3
5
  module Libcall
6
+ # Parse and coerce TYPE VALUE argument pairs for FFI calls
4
7
  class Parser
5
- TYPE_MAP = {
6
- 'i8' => :char,
7
- 'u8' => :uchar,
8
- 'i16' => :short,
9
- 'u16' => :ushort,
10
- 'i32' => :int,
11
- 'u32' => :uint,
12
- 'i64' => :long_long,
13
- 'u64' => :ulong_long,
14
- 'isize' => :long,
15
- 'usize' => :ulong,
16
- 'f32' => :float,
17
- 'f64' => :double,
18
- 'cstr' => :string,
19
- 'ptr' => :voidp,
20
- 'void' => :void,
21
- 'int' => :int,
22
- 'uint' => :uint,
23
- 'long' => :long,
24
- 'ulong' => :ulong,
25
- 'float' => :float,
26
- 'double' => :double,
27
- 'char' => :char,
28
- 'str' => :string
29
- }.freeze
30
-
31
- def self.parse_arg(arg)
32
- return [:string, ''] if arg.empty?
33
-
34
- if (arg.start_with?('"') && arg.end_with?('"')) ||
35
- (arg.start_with?("'") && arg.end_with?("'"))
36
- return [:string, arg[1...-1]]
8
+ # Pair-only API helpers
9
+ def self.parse_type(type_str)
10
+ # Output array spec: out:TYPE[N]
11
+ if type_str.start_with?('out:') && type_str.match(/^out:(.+)\[(\d+)\]$/)
12
+ base = Regexp.last_match(1)
13
+ count = Regexp.last_match(2).to_i
14
+ base_sym = TypeMap.lookup(base)
15
+ raise Error, "Unknown output array type: #{base}" unless base_sym
16
+
17
+ return [:out_array, base_sym, count]
37
18
  end
38
19
 
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)
20
+ # Input array spec: TYPE[] (value as comma-separated list)
21
+ if type_str.end_with?('[]')
22
+ base = type_str[0..-3]
23
+ base_sym = TypeMap.lookup(base)
24
+ raise Error, "Unknown array base type: #{base}" unless base_sym
42
25
 
43
- type_sym = TYPE_MAP[type_str]
44
- raise Error, "Unknown type suffix: #{type_str}" unless type_sym
26
+ return [:array, base_sym]
27
+ end
45
28
 
46
- value = if %i[float double].include?(type_sym)
47
- value_str.to_f
48
- else
49
- value_str.to_i
50
- end
29
+ # Output pointer spec: out:TYPE (e.g., out:int, out:f64)
30
+ if type_str.start_with?('out:')
31
+ inner = type_str.sub(/^out:/, '')
32
+ inner_sym = TypeMap.lookup(inner)
33
+ raise Error, "Unknown type in out: #{inner}" unless inner_sym
51
34
 
52
- return [type_sym, value]
35
+ return [:out, inner_sym]
53
36
  end
54
37
 
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
38
+ type_sym = TypeMap.lookup(type_str)
39
+ raise Error, "Unknown type: #{type_str}" unless type_sym
40
+
41
+ type_sym
62
42
  end
63
43
 
64
44
  def self.parse_return_type(type_str)
65
45
  return :void if type_str.nil? || type_str.empty? || type_str == 'void'
66
46
 
67
- type_sym = TYPE_MAP[type_str]
47
+ type_sym = TypeMap.lookup(type_str)
68
48
  raise Error, "Unknown return type: #{type_str}" unless type_sym
69
49
 
70
50
  type_sym
71
51
  end
72
52
 
73
- def self.parse_signature(sig)
74
- raise Error, "Invalid signature format: #{sig}" unless sig =~ /^([a-z]\w*)\((.*)\)$/i
53
+ def self.coerce_value(type_sym, token)
54
+ # Input array values: comma-separated
55
+ if type_sym.is_a?(Array) && type_sym.first == :array
56
+ base = type_sym[1]
57
+ return [] if token.nil? || token.empty?
58
+
59
+ return token.split(',').map { |t| coerce_single_value(base, t.strip) }
60
+ end
61
+
62
+ case type_sym
63
+ when *TypeMap::FLOAT_TYPES
64
+ Float(token)
65
+ when *TypeMap::INTEGER_TYPES
66
+ Integer(token)
67
+ when :voidp
68
+ # Accept common null tokens for pointer types
69
+ return 0 if token =~ /\A(null|nil|NULL|0)\z/
75
70
 
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}"
71
+ Integer(token)
72
+ when :string
73
+ strip_quotes(token)
74
+ when :void
75
+ raise Error, 'void cannot be used as an argument type'
76
+ else
77
+ raise Error, "Unknown type for coercion: #{type_sym}"
79
78
  end
80
- [ret_type, arg_types]
81
79
  end
82
80
 
83
- def self.fiddle_type(type_sym)
81
+ def self.coerce_single_value(type_sym, token)
84
82
  case type_sym
85
- when :void then Fiddle::TYPE_VOID
86
- when :char then Fiddle::TYPE_CHAR
87
- when :uchar then Fiddle::TYPE_UCHAR
88
- when :short then Fiddle::TYPE_SHORT
89
- when :ushort then Fiddle::TYPE_USHORT
90
- when :int, :uint then Fiddle::TYPE_INT
91
- when :long, :ulong then Fiddle::TYPE_LONG
92
- when :long_long, :ulong_long then Fiddle::TYPE_LONG_LONG
93
- when :float then Fiddle::TYPE_FLOAT
94
- when :double then Fiddle::TYPE_DOUBLE
95
- when :voidp, :string then Fiddle::TYPE_VOIDP
83
+ when *TypeMap::FLOAT_TYPES
84
+ Float(token)
85
+ when *TypeMap::INTEGER_TYPES
86
+ Integer(token)
87
+ when :string
88
+ strip_quotes(token)
89
+ else
90
+ raise Error, "Unknown element type for coercion: #{type_sym}"
91
+ end
92
+ end
93
+
94
+ def self.strip_quotes(token)
95
+ if (token.start_with?('"') && token.end_with?('"')) || (token.start_with?("'") && token.end_with?("'"))
96
+ token[1...-1]
96
97
  else
97
- raise Error, "Unknown Fiddle type: #{type_sym}"
98
+ token
98
99
  end
99
100
  end
100
101
  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,209 @@
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
+ 'short' => :short,
31
+ 'ushort' => :ushort,
32
+ 'int' => :int,
33
+ 'uint' => :uint,
34
+ 'long' => :long,
35
+ 'ulong' => :ulong,
36
+ 'float' => :float,
37
+ 'double' => :double,
38
+ # Extended type names (stdint-like)
39
+ 'int8' => :char,
40
+ 'uint8' => :uchar,
41
+ 'int16' => :short,
42
+ 'uint16' => :ushort,
43
+ 'int32' => :int,
44
+ 'uint32' => :uint,
45
+ 'int64' => :long_long,
46
+ 'uint64' => :ulong_long,
47
+ 'float32' => :float,
48
+ 'float64' => :double,
49
+ # Size and pointer-sized integers
50
+ 'size_t' => :ulong,
51
+ 'ssize_t' => :long,
52
+ 'intptr' => :long,
53
+ 'uintptr' => :ulong,
54
+ 'intptr_t' => :long,
55
+ 'uintptr_t' => :ulong,
56
+ 'ptrdiff_t' => :long,
57
+ # Boolean
58
+ 'bool' => :int,
59
+ # String aliases
60
+ 'str' => :string,
61
+ 'string' => :string
62
+ }.freeze
63
+
64
+ # Integer type symbols
65
+ INTEGER_TYPES = %i[
66
+ int uint
67
+ long ulong
68
+ long_long ulong_long
69
+ char uchar
70
+ short ushort
71
+ ].freeze
72
+
73
+ # Floating point type symbols
74
+ FLOAT_TYPES = %i[float double].freeze
75
+
76
+ # Look up FFI type symbol from string
77
+ def self.lookup(type_str)
78
+ MAP[type_str]
79
+ end
80
+
81
+ # Check if type symbol is an integer type
82
+ def self.integer_type?(type_sym)
83
+ INTEGER_TYPES.include?(type_sym)
84
+ end
85
+
86
+ # Check if type symbol is a floating point type
87
+ def self.float_type?(type_sym)
88
+ FLOAT_TYPES.include?(type_sym)
89
+ end
90
+
91
+ # Convert type symbol to Fiddle type constant
92
+ def self.to_fiddle_type(type_sym)
93
+ # Array and output parameters are passed as pointers
94
+ if type_sym.is_a?(Array)
95
+ tag = type_sym.first
96
+ return Fiddle::TYPE_VOIDP if %i[out array out_array].include?(tag)
97
+ end
98
+
99
+ case type_sym
100
+ when :void then Fiddle::TYPE_VOID
101
+ when :char then Fiddle::TYPE_CHAR
102
+ when :uchar then Fiddle::TYPE_UCHAR
103
+ when :short then Fiddle::TYPE_SHORT
104
+ when :ushort then Fiddle::TYPE_USHORT
105
+ when :int, :uint then Fiddle::TYPE_INT
106
+ when :long, :ulong then Fiddle::TYPE_LONG
107
+ when :long_long, :ulong_long then Fiddle::TYPE_LONG_LONG
108
+ when :float then Fiddle::TYPE_FLOAT
109
+ when :double then Fiddle::TYPE_DOUBLE
110
+ when :voidp, :string then Fiddle::TYPE_VOIDP
111
+ else
112
+ raise Error, "Unknown Fiddle type: #{type_sym}"
113
+ end
114
+ end
115
+
116
+ # Get the size in bytes for a type symbol
117
+ def self.sizeof(type_sym)
118
+ case type_sym
119
+ when :char, :uchar then Fiddle::SIZEOF_CHAR
120
+ when :short, :ushort then Fiddle::SIZEOF_SHORT
121
+ when :int, :uint then Fiddle::SIZEOF_INT
122
+ when :long, :ulong then Fiddle::SIZEOF_LONG
123
+ when :long_long, :ulong_long then Fiddle::SIZEOF_LONG_LONG
124
+ when :float then Fiddle::SIZEOF_FLOAT
125
+ when :double then Fiddle::SIZEOF_DOUBLE
126
+ when :voidp, :string then Fiddle::SIZEOF_VOIDP
127
+ else
128
+ raise Error, "Cannot get size for type: #{type_sym}"
129
+ end
130
+ end
131
+
132
+ # Allocate a pointer for output parameter
133
+ def self.allocate_output_pointer(type_sym)
134
+ ptr = Fiddle::Pointer.malloc(sizeof(type_sym))
135
+ # For out:string, we pass char**. Initialize inner pointer to NULL for safety.
136
+ ptr[0, Fiddle::SIZEOF_VOIDP] = [0].pack('J') if type_sym == :string
137
+ ptr
138
+ end
139
+
140
+ # Read value from output pointer
141
+ def self.read_output_pointer(ptr, type_sym)
142
+ case type_sym
143
+ when :char then ptr[0, Fiddle::SIZEOF_CHAR].unpack1('c')
144
+ when :uchar then ptr[0, Fiddle::SIZEOF_CHAR].unpack1('C')
145
+ when :short then ptr[0, Fiddle::SIZEOF_SHORT].unpack1('s')
146
+ when :ushort then ptr[0, Fiddle::SIZEOF_SHORT].unpack1('S')
147
+ when :int then ptr[0, Fiddle::SIZEOF_INT].unpack1('i')
148
+ when :uint then ptr[0, Fiddle::SIZEOF_INT].unpack1('I')
149
+ when :long then ptr[0, Fiddle::SIZEOF_LONG].unpack1('l!')
150
+ when :ulong then ptr[0, Fiddle::SIZEOF_LONG].unpack1('L!')
151
+ when :long_long then ptr[0, Fiddle::SIZEOF_LONG_LONG].unpack1('q')
152
+ when :ulong_long then ptr[0, Fiddle::SIZEOF_LONG_LONG].unpack1('Q')
153
+ when :float then ptr[0, Fiddle::SIZEOF_FLOAT].unpack1('f')
154
+ when :double then ptr[0, Fiddle::SIZEOF_DOUBLE].unpack1('d')
155
+ when :string
156
+ addr = ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J')
157
+ return '(null)' if addr.zero?
158
+
159
+ begin
160
+ Fiddle::Pointer.new(addr).to_s
161
+ rescue StandardError
162
+ format('0x%x', addr)
163
+ end
164
+ when :voidp then format('0x%x', ptr[0, Fiddle::SIZEOF_VOIDP].unpack1('J'))
165
+ else
166
+ raise Error, "Cannot read output value for type: #{type_sym}"
167
+ end
168
+ end
169
+
170
+ # Allocate memory for an array of base type and count elements
171
+ def self.allocate_array(base_type, count)
172
+ Fiddle::Pointer.malloc(sizeof(base_type) * count)
173
+ end
174
+
175
+ def self.write_array(ptr, base_type, values)
176
+ return if values.nil? || values.empty?
177
+
178
+ bytes = sizeof(base_type) * values.length
179
+ ptr[0, bytes] = values.pack(pack_template(base_type) + values.length.to_s)
180
+ end
181
+
182
+ def self.read_array(ptr, base_type, count)
183
+ return [] if count <= 0
184
+
185
+ bytes = sizeof(base_type) * count
186
+ raw = ptr[0, bytes]
187
+ raw.unpack(pack_template(base_type) + count.to_s)
188
+ end
189
+
190
+ def self.pack_template(base_type)
191
+ case base_type
192
+ when :char then 'c'
193
+ when :uchar then 'C'
194
+ when :short then 's'
195
+ when :ushort then 'S'
196
+ when :int then 'i'
197
+ when :uint then 'I'
198
+ when :long then 'l!'
199
+ when :ulong then 'L!'
200
+ when :long_long then 'q'
201
+ when :ulong_long then 'Q'
202
+ when :float then 'f'
203
+ when :double then 'd'
204
+ else
205
+ raise Error, "Unsupported array base type: #{base_type}"
206
+ end
207
+ end
208
+ end
209
+ end
@@ -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.2'
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.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - kojix2
@@ -51,6 +51,8 @@ files:
51
51
  - lib/libcall/cli.rb
52
52
  - lib/libcall/library_finder.rb
53
53
  - lib/libcall/parser.rb
54
+ - lib/libcall/platform.rb
55
+ - lib/libcall/type_map.rb
54
56
  - lib/libcall/version.rb
55
57
  homepage: https://github.com/kojix2/libcall
56
58
  licenses: