libcall 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b45f825f6989bd4673b982435ce1bad8ff1b1785785118ffa66c8bdad84b1d7c
4
- data.tar.gz: 9a60c1d6d69b2ff7e70a2884b6dfbc8e2b6099c87df40558559fdffffff5c65c
3
+ metadata.gz: db2f2234f00412d76e307fc12179b0a829953a08bd6aefa83d59bbab866e85f3
4
+ data.tar.gz: bb2cd9b42fb09fc6b6e5201ed98c16dae47258e9a1fb23ef76fc6a49efe17ab3
5
5
  SHA512:
6
- metadata.gz: ed0cde569e1eea83480b744907d3397f6e7abae68b991993090312edc6ccf74d4cf973afbad2dd3402142eccf390a3f992cf2174ad016ea0009d5d8b91e104bd
7
- data.tar.gz: 42571c69fd4e1487e17b592ec98afc321ed646e938d9d773a96c4e450f4762c9d49975f7de47a33ba5f747e2e2c1367da00d7a3233ff059a8ab5a70f43a3bb92
6
+ metadata.gz: 12bc58798f6130a316ba87e53a66d2806035a7fd8abbb6ba2fc71654d82e0f2210149152cf9876ca818a5241808a2c871021eca2c7264a2110cfe37199da2872
7
+ data.tar.gz: bb6f252e8a7a9aec0a353abec0ed9674b346bb1af3471768ca51fad040234024c6fc77b18a25d42c396a389a0063ac44c5675f54fa8bd66eed3b0c9055da8e34
data/README.md CHANGED
@@ -23,13 +23,11 @@ libcall [OPTIONS] <LIBRARY> <FUNCTION> (TYPE VALUE)...
23
23
  ### Quick Examples
24
24
 
25
25
  ```sh
26
- # TYPE VALUE pairs
27
- libcall -lm -r f64 sqrt double 16
28
- # => 4.0
26
+ libcall -lm sqrt double 16 -r double # => 4.0
27
+ ```
29
28
 
30
- # libc strlen
31
- libcall -lc strlen string "hello" -r usize
32
- # => 5
29
+ ```sh
30
+ libcall -lc strlen string "hello" -r usize # => 5
33
31
  ```
34
32
 
35
33
  ### Argument Syntax
@@ -76,25 +74,51 @@ Library search:
76
74
 
77
75
  ### More Examples
78
76
 
77
+ TYPE/VALUE pairs with `-r` before function
78
+
79
+ ```sh
80
+ libcall -lm -r double fabs double -5.5 # => 5.5
81
+ ```
82
+
83
+ Output parameter with libm
84
+
85
+ ```sh
86
+ libcall -lm modf double -3.14 out:double -r f64
87
+ # Result: -0.14000000000000012
88
+ # Output parameters:
89
+ # [1] double = -3.0
90
+ ```
91
+
92
+ JSON output
93
+
79
94
  ```sh
80
- # JSON output
81
95
  libcall --json -lm sqrt double 9.0 -r f64
96
+ # {
97
+ # "library": "/lib/x86_64-linux-gnu/libm.so",
98
+ # "function": "sqrt",
99
+ # "return_type": "double",
100
+ # "result": 3.0
101
+ # }
102
+ ```
103
+
104
+ Dry run
82
105
 
83
- # Dry run
106
+ ```sh
84
107
  libcall --dry-run -lc getpid -r int
108
+ # Library: /lib/x86_64-linux-gnu/libc.so
109
+ # Function: getpid
110
+ # Return: int
111
+ ```
85
112
 
86
- # Output parameter with libm
87
- libcall -lm modf double -3.14 out:double -r f64
113
+ Windows: calling C runtime functions
88
114
 
89
- # TYPE/VALUE pairs with -r after function
90
- libcall -lm fabs double -5.5 -r f64
91
- # => 5.5
115
+ ```powershell
116
+ libcall msvcrt.dll sqrt double 16.0 -r f64 # => 4.0
117
+ ```
92
118
 
93
- # Windows: calling C runtime functions
94
- libcall msvcrt.dll sqrt double 16.0 -r f64
95
- # => 4.0
119
+ Windows: accessing environment variables
96
120
 
97
- # Windows: accessing environment variables
121
+ ```powershell
98
122
  libcall msvcrt.dll getenv string "PATH" -r cstr
99
123
  ```
100
124
 
@@ -119,6 +143,8 @@ Also supported:
119
143
  - `cstr`: C string return (char\*)
120
144
  - `ptr`/`pointer`: void\* pointer
121
145
 
146
+ See [type_map.rb](lib/libcall/type_map.rb) for all available type mappings.
147
+
122
148
  ## pkg-config Support
123
149
 
124
150
  Set `PKG_CONFIG_PATH` and use package names with `-l`:
@@ -160,6 +186,42 @@ libcall -lc getrandom out:uchar[16] size_t 16 uint 0 -r long
160
186
  libcall -lSystem arc4random_buf out:uchar[16] size_t 16 -r void
161
187
  ```
162
188
 
189
+ ## Callbacks (experimental)
190
+
191
+ Pass a C function pointer via a Ruby callback. Use `func` or `callback` with a quoted spec:
192
+
193
+ - Syntax: `func 'RET(ARG,ARG,...){|a, b, ...| ruby_code }'` (alias: `callback ...`)
194
+ - Inside the block, helper methods from `Libcall::Fiddley::DSL` are available:
195
+ - `int(ptr)`, `double(ptr)`, `cstr(ptr)` read values from pointers
196
+ - `read(:type, ptr)` reads any supported type; `ptr(addr)` makes a pointer
197
+
198
+ Quick examples
199
+
200
+ ```sh
201
+ # Fixture function: int32_t apply_i32(int32_t, int32_t, int32_t (*)(int32_t,int32_t))
202
+ libcall -ltest -L test/fixtures/libtest/build apply_i32 \
203
+ int 3 int 5 \
204
+ func 'int(int,int){|a,b| a + b}' \
205
+ -r i32
206
+ # => 8
207
+ ```
208
+
209
+ ```sh
210
+ # libc qsort: sort 4 ints ascending; use out:int[4] with an initializer so the result prints
211
+ libcall -lc qsort \
212
+ out:int[4] 4,2,3,1 \
213
+ size_t 4 \
214
+ size_t 4 \
215
+ callback 'int(void*,void*){|pa,pb| int(pa) <=> int(pb) }' \
216
+ -r void
217
+ # Output parameters:
218
+ # [0] int[4] = [1, 2, 3, 4]
219
+ ```
220
+
221
+ Notes
222
+
223
+ - Match the C signature exactly (types and arity). Blocks run in-process; exceptions abort the call.
224
+
163
225
  ## Warning
164
226
 
165
227
  FFI calls are inherently unsafe. You must:
@@ -18,6 +18,7 @@ module Libcall
18
18
  arg_types = []
19
19
  arg_values = []
20
20
  out_refs = []
21
+ closures = []
21
22
 
22
23
  arg_pairs.each_with_index do |(type_sym, value), idx|
23
24
  arg_types << TypeMap.to_fiddle_type(type_sym)
@@ -39,11 +40,47 @@ module Libcall
39
40
  base = type_sym[1]
40
41
  count = type_sym[2]
41
42
  ptr = TypeMap.allocate_array(base, count)
43
+ # Optional initializer values
44
+ if value
45
+ vals = Array(value)
46
+ raise Error, "Initializer length #{vals.length} does not match out array size #{count}" unless vals.length == count
47
+ TypeMap.write_array(ptr, base, vals)
48
+ end
42
49
  out_refs << { index: idx, kind: :out_array, base: base, count: count, ptr: ptr }
43
50
  arg_values << ptr.to_i
44
51
  else
45
52
  raise Error, "Unknown array/output form: #{type_sym.inspect}"
46
53
  end
54
+ elsif type_sym == :callback
55
+ spec = value
56
+ unless spec.is_a?(Hash) && spec[:kind] == :callback
57
+ raise Error, 'Invalid callback value; expected func signature and block'
58
+ end
59
+
60
+ ret_ty = TypeMap.to_fiddle_type(spec[:ret])
61
+ arg_tys = spec[:args].map { |a| TypeMap.to_fiddle_type(a) }
62
+ # Build Ruby proc from block source, e.g., "{|a,b| a+b}"
63
+ # Evaluate proc in a helper context so DSL methods are available
64
+ ctx = Object.new.extend(Libcall::Fiddley::DSL)
65
+ begin
66
+ ruby_proc = ctx.instance_eval("proc #{spec[:block]}", __FILE__, __LINE__)
67
+ rescue SyntaxError => e
68
+ raise Error, "Invalid Ruby block for callback: #{e.message}"
69
+ end
70
+ closure = Fiddle::Closure::BlockCaller.new(ret_ty, arg_tys) do |*cb_args|
71
+ # Convert pointer-typed args to Fiddle::Pointer for convenience
72
+ cooked = cb_args.each_with_index.map do |v, i|
73
+ at = spec[:args][i]
74
+ if at == :voidp
75
+ Fiddle::Pointer.new(v)
76
+ else
77
+ v
78
+ end
79
+ end
80
+ ruby_proc.call(*cooked)
81
+ end
82
+ closures << closure # keep alive during call
83
+ arg_values << closure
47
84
  else
48
85
  arg_values << value
49
86
  end
data/lib/libcall/cli.rb CHANGED
@@ -156,8 +156,17 @@ module Libcall
156
156
  type_sym = Parser.parse_type(type_tok)
157
157
 
158
158
  # TYPE that represents an output pointer/array does not require a value
159
+ # For out:TYPE[N], allow an optional comma-separated initializer list right after the type.
159
160
  if type_sym.is_a?(Array) && %i[out out_array].include?(type_sym.first)
160
- arg_pairs << [type_sym, nil]
161
+ if type_sym.first == :out_array && i < argv.length && argv[i].include?(',')
162
+ init_tok = argv[i]
163
+ i += 1
164
+ base = type_sym[1]
165
+ values = Parser.coerce_value([:array, base], init_tok)
166
+ arg_pairs << [type_sym, values]
167
+ else
168
+ arg_pairs << [type_sym, nil]
169
+ end
161
170
  next
162
171
  end
163
172
 
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A tiny helper layer inspired by fiddley (BSD-2-Clause) to offer
4
+ # convenient, DSL-friendly utilities on top of Ruby's Fiddle.
5
+ # This is intentionally small and tailored for libcall use-cases.
6
+
7
+ require 'fiddle'
8
+
9
+ module Libcall
10
+ module Fiddley
11
+ # Small helper methods intended for use inside callback blocks
12
+ module DSL
13
+ module_function
14
+
15
+ def ptr(x)
16
+ Fiddle::Pointer.new(Integer(x))
17
+ end
18
+
19
+ def read(type, p)
20
+ t = type.is_a?(Symbol) ? type : type.to_s
21
+ t_sym = Libcall::TypeMap.lookup(t) || (type.is_a?(Symbol) ? type : nil)
22
+ raise Libcall::Error, "unknown read type: #{type}" unless t_sym
23
+
24
+ pp = p.is_a?(Fiddle::Pointer) ? p : Fiddle::Pointer.new(Integer(p))
25
+ Libcall::TypeMap.read_output_pointer(pp, t_sym)
26
+ end
27
+
28
+ # Typed short-hands
29
+ def char(p) = read(:char, p)
30
+ def uchar(p) = read(:uchar, p)
31
+ def short(p) = read(:short, p)
32
+ def ushort(p) = read(:ushort, p)
33
+ def int(p) = read(:int, p)
34
+ def uint(p) = read(:uint, p)
35
+ def long(p) = read(:long, p)
36
+ def ulong(p) = read(:ulong, p)
37
+ def long_long(p) = read(:long_long, p)
38
+ def ulong_long(p) = read(:ulong_long, p)
39
+ def float(p) = read(:float, p)
40
+ def double(p) = read(:double, p)
41
+ def cstr(p) = read(:string, p)
42
+ end
43
+
44
+ module Utils
45
+ module_function
46
+
47
+ # Native size_t pack template and size
48
+ SIZET_PACK = (Fiddle::SIZEOF_VOIDP == Fiddle::SIZEOF_LONG ? 'L!' : 'Q')
49
+
50
+ # Return size in bytes for a given type symbol.
51
+ # Falls back to pointer size for :pointer and :voidp.
52
+ def sizeof(type)
53
+ return Fiddle::SIZEOF_SIZE_T if type == :size_t
54
+ return Fiddle::SIZEOF_VOIDP if %i[pointer voidp].include?(type)
55
+
56
+ # Delegate to Libcall::TypeMap when possible
57
+ Libcall::TypeMap.sizeof(type)
58
+ rescue StandardError
59
+ raise Libcall::Error, "unknown type for sizeof: #{type}"
60
+ end
61
+
62
+ # Convert a type symbol to a Fiddle type constant.
63
+ def to_fiddle_type(type)
64
+ return Fiddle::TYPE_SIZE_T if type == :size_t
65
+ return Fiddle::TYPE_VOIDP if %i[pointer voidp].include?(type)
66
+
67
+ Libcall::TypeMap.to_fiddle_type(type)
68
+ rescue StandardError
69
+ raise Libcall::Error, "unknown type for to_fiddle_type: #{type}"
70
+ end
71
+
72
+ # Pack template for array values of given base type.
73
+ def array_pack_template(type)
74
+ return SIZET_PACK if type == :size_t
75
+
76
+ # Use TypeMap's packing for standard types
77
+ Libcall::TypeMap.pack_template(type)
78
+ rescue StandardError
79
+ # For generic pointers/addresses, use native unsigned pointer width
80
+ return 'J' if %i[pointer voidp].include?(type)
81
+
82
+ raise Libcall::Error, "Unsupported array base type: #{type}"
83
+ end
84
+
85
+ # Convert Ruby array of numbers to a binary string for the given type
86
+ def array2str(type, array)
87
+ array.pack("#{array_pack_template(type)}*")
88
+ end
89
+
90
+ # Convert binary string to Ruby array of the given type
91
+ def str2array(type, str)
92
+ str.unpack("#{array_pack_template(type)}*")
93
+ end
94
+ end
95
+
96
+ # Minimal memory buffer wrapper to make building args/arrays convenient
97
+ class MemoryPointer
98
+ attr_reader :size
99
+
100
+ def initialize(type, count = 1)
101
+ @type = type
102
+ @size = Utils.sizeof(type) * count
103
+ @ptr = Fiddle::Pointer.malloc(@size)
104
+ end
105
+
106
+ def to_ptr
107
+ @ptr
108
+ end
109
+
110
+ def address
111
+ @ptr.to_i
112
+ end
113
+
114
+ def write_array(type, values)
115
+ data = Utils.array2str(type, Array(values))
116
+ @ptr[0, data.bytesize] = data
117
+ self
118
+ end
119
+
120
+ def read_array(type, count)
121
+ bytes = Utils.sizeof(type) * count
122
+ Utils.str2array(type, @ptr[0, bytes])
123
+ end
124
+
125
+ def put_bytes(offset, str)
126
+ @ptr[offset, str.bytesize] = str
127
+ end
128
+
129
+ def write_bytes(str)
130
+ put_bytes(0, str)
131
+ end
132
+
133
+ def get_bytes(offset, len)
134
+ @ptr[offset, len]
135
+ end
136
+
137
+ def read_bytes(len)
138
+ get_bytes(0, len)
139
+ end
140
+
141
+ # Return Fiddle::Pointer stored at this pointer (read void*)
142
+ def read_pointer
143
+ to_ptr.ptr
144
+ end
145
+ end
146
+
147
+ # Wrap Fiddle::Closure::BlockCaller with friendlier type mapping
148
+ class Function < Fiddle::Closure::BlockCaller
149
+ def initialize(ret, params, &blk)
150
+ r = Utils.to_fiddle_type(ret)
151
+ p = Array(params).map { |t| Utils.to_fiddle_type(t) }
152
+ super(r, p, &blk)
153
+ end
154
+ end
155
+ end
156
+ end
@@ -7,6 +7,9 @@ module Libcall
7
7
  class Parser
8
8
  # Pair-only API helpers
9
9
  def self.parse_type(type_str)
10
+ # Callback function pointer: func/callback 'ret(arg,...) { |...| ... }'
11
+ return :callback if %w[func callback].include?(type_str)
12
+
10
13
  # Output array spec: out:TYPE[N]
11
14
  if type_str.start_with?('out:') && type_str.match(/^out:(.+)\[(\d+)\]$/)
12
15
  base = Regexp.last_match(1)
@@ -51,6 +54,32 @@ module Libcall
51
54
  end
52
55
 
53
56
  def self.coerce_value(type_sym, token)
57
+ # Callback value: signature + Ruby block
58
+ if type_sym == :callback
59
+ src = strip_quotes(token.to_s)
60
+ m = src.match(/\A\s*([^(\s]+)\s*\(([^)]*)\)\s*(\{.*\})\s*\z/m)
61
+ raise Error, "Invalid callback spec: #{src}" unless m
62
+
63
+ ret_s = m[1].strip
64
+ args_s = m[2].strip
65
+ block_src = m[3]
66
+
67
+ ret_sym = TypeMap.lookup(ret_s)
68
+ raise Error, "Unknown callback return type: #{ret_s}" unless ret_sym
69
+
70
+ arg_syms = if args_s.empty?
71
+ []
72
+ else
73
+ args_s.split(',').map(&:strip).map do |a|
74
+ sym = TypeMap.lookup(a)
75
+ raise Error, "Unknown callback arg type: #{a}" unless sym
76
+
77
+ sym
78
+ end
79
+ end
80
+
81
+ return { kind: :callback, ret: ret_sym, args: arg_syms, block: block_src }
82
+ end
54
83
  # Input array values: comma-separated
55
84
  if type_sym.is_a?(Array) && type_sym.first == :array
56
85
  base = type_sym[1]
@@ -27,6 +27,7 @@ module Libcall
27
27
  'void' => :void,
28
28
  # Common C type names
29
29
  'char' => :char,
30
+ 'uchar' => :uchar,
30
31
  'short' => :short,
31
32
  'ushort' => :ushort,
32
33
  'int' => :int,
@@ -35,6 +36,21 @@ module Libcall
35
36
  'ulong' => :ulong,
36
37
  'float' => :float,
37
38
  'double' => :double,
39
+ # C-style pointer aliases
40
+ 'void*' => :voidp,
41
+ 'const void*' => :voidp,
42
+ 'const_void*' => :voidp,
43
+ 'const_voidp' => :voidp,
44
+ # Underscored variants
45
+ 'unsigned_char' => :uchar,
46
+ 'unsigned_short' => :ushort,
47
+ 'unsigned_int' => :uint,
48
+ 'unsigned_long' => :ulong,
49
+ 'long_long' => :long_long,
50
+ 'unsigned_long_long' => :ulong_long,
51
+ # Short aliases
52
+ 'unsigned' => :uint,
53
+ 'signed' => :int,
38
54
  # Extended type names (stdint-like)
39
55
  'int8' => :char,
40
56
  'uint8' => :uchar,
@@ -46,6 +62,15 @@ module Libcall
46
62
  'uint64' => :ulong_long,
47
63
  'float32' => :float,
48
64
  'float64' => :double,
65
+ # C99/C11 standard types with _t suffix
66
+ 'int8_t' => :char,
67
+ 'uint8_t' => :uchar,
68
+ 'int16_t' => :short,
69
+ 'uint16_t' => :ushort,
70
+ 'int32_t' => :int,
71
+ 'uint32_t' => :uint,
72
+ 'int64_t' => :long_long,
73
+ 'uint64_t' => :ulong_long,
49
74
  # Size and pointer-sized integers
50
75
  'size_t' => :ulong,
51
76
  'ssize_t' => :long,
@@ -96,6 +121,9 @@ module Libcall
96
121
  return Fiddle::TYPE_VOIDP if %i[out array out_array].include?(tag)
97
122
  end
98
123
 
124
+ # Callback function pointers are passed as void*
125
+ return Fiddle::TYPE_VOIDP if type_sym == :callback
126
+
99
127
  case type_sym
100
128
  when :void then Fiddle::TYPE_VOID
101
129
  when :char then Fiddle::TYPE_CHAR
@@ -167,6 +195,11 @@ module Libcall
167
195
  end
168
196
  end
169
197
 
198
+ # Read a single scalar value at address (helper for callbacks)
199
+ def self.read_scalar(ptr, type_sym)
200
+ read_output_pointer(ptr, type_sym)
201
+ end
202
+
170
203
  # Allocate memory for an array of base type and count elements
171
204
  def self.allocate_array(base_type, count)
172
205
  Fiddle::Pointer.malloc(sizeof(base_type) * count)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Libcall
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.3'
5
5
  end
data/lib/libcall.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'libcall/parser'
5
5
  require_relative 'libcall/library_finder'
6
6
  require_relative 'libcall/caller'
7
7
  require_relative 'libcall/cli'
8
+ require_relative 'libcall/fiddley'
8
9
 
9
10
  module Libcall
10
11
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: libcall
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - kojix2
@@ -49,6 +49,7 @@ files:
49
49
  - lib/libcall.rb
50
50
  - lib/libcall/caller.rb
51
51
  - lib/libcall/cli.rb
52
+ - lib/libcall/fiddley.rb
52
53
  - lib/libcall/library_finder.rb
53
54
  - lib/libcall/parser.rb
54
55
  - lib/libcall/platform.rb
@@ -72,7 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
73
  - !ruby/object:Gem::Version
73
74
  version: '0'
74
75
  requirements: []
75
- rubygems_version: 3.6.9
76
+ rubygems_version: 3.7.2
76
77
  specification_version: 4
77
78
  summary: Call functions in shared libraries directly from the CLI
78
79
  test_files: []