libcall 0.0.1 → 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: 39da3018356d7d6e8287d59bb0ca8ccc24f5a8b211d974b51d526d21347ac0f5
4
- data.tar.gz: 6d55af828e7e621226bab1efff15174f00e1c542af390cf8e5886f1bbd8369c1
3
+ metadata.gz: b45f825f6989bd4673b982435ce1bad8ff1b1785785118ffa66c8bdad84b1d7c
4
+ data.tar.gz: 9a60c1d6d69b2ff7e70a2884b6dfbc8e2b6099c87df40558559fdffffff5c65c
5
5
  SHA512:
6
- metadata.gz: bda1f14c0ae9d52f1b8edff83a8c949df3742f12e280ba87c562c1dc958507d329b934c3a87a96d3b14975a2751852bc3498e3c4e99fab3963e32888e81c4e57
7
- data.tar.gz: 3621dad8e6ad6d5e3ea3ba52970d87457f4fb3dcb03824f75bd88148424a150ff6e6d4565fe1780c859ffc3787750f78905c8afa4fb24e87341b8a90ab3edd74
6
+ metadata.gz: ed0cde569e1eea83480b744907d3397f6e7abae68b991993090312edc6ccf74d4cf973afbad2dd3402142eccf390a3f992cf2174ad016ea0009d5d8b91e104bd
7
+ data.tar.gz: 42571c69fd4e1487e17b592ec98afc321ed646e938d9d773a96c4e450f4762c9d49975f7de47a33ba5f747e2e2c1367da00d7a3233ff059a8ab5a70f43a3bb92
data/README.md CHANGED
@@ -27,9 +27,9 @@ libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
27
27
  libcall -lm -r f64 sqrt double 16
28
28
  # => 4.0
29
29
 
30
- # Custom library
31
- libcall ./mylib.so add_i32 int 10 int 20 -r i32
32
- # => 30
30
+ # libc strlen
31
+ libcall -lc strlen string "hello" -r usize
32
+ # => 5
33
33
  ```
34
34
 
35
35
  ### Argument Syntax
@@ -63,7 +63,6 @@ libcall -lc getenv string -- -r -r cstr
63
63
  - `-l LIBRARY` - library name (searches standard paths)
64
64
  - `-L PATH` - add library search path
65
65
  - `-r TYPE` - return type (void, i32, f64, cstr, ptr)
66
- - Options may appear before or after the function name.
67
66
  - `--dry-run` - validate without executing
68
67
  - `--json` - JSON output
69
68
  - `--verbose` - detailed info
@@ -82,10 +81,10 @@ Library search:
82
81
  libcall --json -lm sqrt double 9.0 -r f64
83
82
 
84
83
  # Dry run
85
- libcall --dry-run ./mylib.so test u64 42 -r void
84
+ libcall --dry-run -lc getpid -r int
86
85
 
87
- # Using -L and -l (like gcc)
88
- libcall -lmylib -L./build add_i32 int 10 int 20 -r i32
86
+ # Output parameter with libm
87
+ libcall -lm modf double -3.14 out:double -r f64
89
88
 
90
89
  # TYPE/VALUE pairs with -r after function
91
90
  libcall -lm fabs double -5.5 -r f64
@@ -101,18 +100,18 @@ libcall msvcrt.dll getenv string "PATH" -r cstr
101
100
 
102
101
  ## Type Reference
103
102
 
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 |
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 |
116
115
 
117
116
  Also supported:
118
117
 
@@ -133,11 +132,32 @@ PKG_CONFIG_PATH=/path/to/pkgconfig libcall -lmypackage func i32 42 -r i32
133
132
  You can pass output pointers by specifying `out:TYPE`. The pointer is allocated automatically, passed to the function, and printed after the call.
134
133
 
135
134
  ```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
135
+ # double frexp(double x, int* exp)
136
+ libcall -lm frexp double 8.0 out:int -r f64
138
137
 
139
138
  # JSON includes an "outputs" array
140
- libcall --json -ltest -L ./test/fixtures/libtest/build get_version out:int out:int -r void
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
141
161
  ```
142
162
 
143
163
  ## Warning
@@ -20,19 +20,36 @@ module Libcall
20
20
  out_refs = []
21
21
 
22
22
  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
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
30
47
  else
31
48
  arg_values << value
32
49
  end
33
50
  end
34
51
 
35
- ret_type = Parser.fiddle_type(return_type)
52
+ ret_type = TypeMap.to_fiddle_type(return_type)
36
53
 
37
54
  handle = Fiddle.dlopen(lib_path)
38
55
  func_ptr = handle[func_name]
@@ -74,43 +91,20 @@ module Libcall
74
91
  end
75
92
  end
76
93
 
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
94
  def read_output_values(out_refs)
94
95
  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 }
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
114
108
  end
115
109
  end
116
110
  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,8 +155,8 @@ 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
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)
127
160
  arg_pairs << [type_sym, nil]
128
161
  next
129
162
  end
@@ -256,18 +289,16 @@ module Libcall
256
289
  end
257
290
 
258
291
  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
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]}"
267
298
  end
268
- else
269
- puts result unless result.nil?
270
299
  end
300
+ else
301
+ puts result unless result.nil?
271
302
  end
272
303
  end
273
304
  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,41 @@
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
+ # 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]
18
+ end
19
+
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
25
+
26
+ return [:array, base_sym]
27
+ end
28
+
40
29
  # Output pointer spec: out:TYPE (e.g., out:int, out:f64)
41
30
  if type_str.start_with?('out:')
42
31
  inner = type_str.sub(/^out:/, '')
43
- inner_sym = TYPE_MAP[inner]
32
+ inner_sym = TypeMap.lookup(inner)
44
33
  raise Error, "Unknown type in out: #{inner}" unless inner_sym
34
+
45
35
  return [:out, inner_sym]
46
36
  end
47
37
 
48
- type_sym = TYPE_MAP[type_str]
38
+ type_sym = TypeMap.lookup(type_str)
49
39
  raise Error, "Unknown type: #{type_str}" unless type_sym
50
40
 
51
41
  type_sym
@@ -54,17 +44,25 @@ module Libcall
54
44
  def self.parse_return_type(type_str)
55
45
  return :void if type_str.nil? || type_str.empty? || type_str == 'void'
56
46
 
57
- type_sym = TYPE_MAP[type_str]
47
+ type_sym = TypeMap.lookup(type_str)
58
48
  raise Error, "Unknown return type: #{type_str}" unless type_sym
59
49
 
60
50
  type_sym
61
51
  end
62
52
 
63
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
+
64
62
  case type_sym
65
- when *FLOAT_TYPES
63
+ when *TypeMap::FLOAT_TYPES
66
64
  Float(token)
67
- when *INTEGER_TYPES
65
+ when *TypeMap::INTEGER_TYPES
68
66
  Integer(token)
69
67
  when :voidp
70
68
  # Accept common null tokens for pointer types
@@ -80,32 +78,24 @@ module Libcall
80
78
  end
81
79
  end
82
80
 
83
- def self.strip_quotes(token)
84
- if (token.start_with?('"') && token.end_with?('"')) || (token.start_with?("'") && token.end_with?("'"))
85
- token[1...-1]
81
+ def self.coerce_single_value(type_sym, token)
82
+ case type_sym
83
+ when *TypeMap::FLOAT_TYPES
84
+ Float(token)
85
+ when *TypeMap::INTEGER_TYPES
86
+ Integer(token)
87
+ when :string
88
+ strip_quotes(token)
86
89
  else
87
- token
90
+ raise Error, "Unknown element type for coercion: #{type_sym}"
88
91
  end
89
92
  end
90
93
 
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
94
+ def self.strip_quotes(token)
95
+ if (token.start_with?('"') && token.end_with?('"')) || (token.start_with?("'") && token.end_with?("'"))
96
+ token[1...-1]
107
97
  else
108
- raise Error, "Unknown Fiddle type: #{type_sym}"
98
+ token
109
99
  end
110
100
  end
111
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.1'
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.1
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: