rubita 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cc492f76a821f0b1b802564b280f4a99162f8d76855ffce00e644906003bdbe5
4
+ data.tar.gz: cec711f5a8c8802a40840b6d3bc5152115e84fe3e55e5d18f8a835e3184c2d2b
5
+ SHA512:
6
+ metadata.gz: 676b956444b8497f5c885cb6ecb627df93ca489d7db63fbe587f3790afc92f8d185dfc2000155ab774ab9209fe3a12820c5ee3ced6ca8a7b801bf01a1a4d0d98
7
+ data.tar.gz: dffbb32c61c04ba730524c610aee6978157bdc0aab9725c50566c504252ec250c6f225d171d5e6ca4391f60949caeaecd9af7e4447922a4594af92e4deb41966
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # Rubita
2
+
3
+ Rubita is a transpiler that converts a restricted Ruby DSL into BCC-compatible C code for eBPF programs.
4
+
5
+ ## Overview
6
+
7
+ Rubita enables you to write eBPF probes and kernel tracing programs using a Ruby-like syntax, which are then transpiled into BCC (Berkeley Packet Filter Compiler Collection) compatible C code. This makes it easier to write complex eBPF programs while leveraging Ruby's expressiveness.
8
+
9
+ ### Supported Features
10
+
11
+ - **Map Declarations**: Define eBPF hash maps with `BPF_HASH`
12
+ - **Probe Definitions**: Support for `TRACEPOINT_PROBE`, `KFUNC_PROBE`, `KRETFUNC_PROBE`, and `LSM_PROBE`
13
+ - **Method Definitions**: Define helper functions with `def`
14
+ - **Field Access**: Convert Ruby dot notation (`obj.field`) to C pointer dereference (`obj->field`)
15
+ - **String Literals**: Support format strings with proper escaping
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```bash
22
+ gem 'rubita', github: 'udzura/rubita'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ ## Basic Conversion
32
+
33
+ ### Hash Map Declaration
34
+
35
+ **Ruby DSL:**
36
+ ```ruby
37
+ BPF_HASH :events, key: :u64, value: :u64, size: 1024
38
+ ```
39
+
40
+ **Generated C:**
41
+ ```c
42
+ BPF_HASH(events, u64, u64, 1024);
43
+ ```
44
+
45
+ ### Tracepoint Probe
46
+
47
+ **Ruby DSL:**
48
+ ```ruby
49
+ TRACEPOINT_PROBE :syscalls, :sys_enter_open do
50
+ bpf_trace_printk("open syscall\n")
51
+ 0
52
+ end
53
+ ```
54
+
55
+ **Generated C:**
56
+ ```c
57
+ TRACEPOINT_PROBE(syscalls, sys_enter_open) {
58
+ bpf_trace_printk("open syscall\n");
59
+ return 0;
60
+ }
61
+ ```
62
+
63
+ ### Kernel Function Probe
64
+
65
+ **Ruby DSL:**
66
+ ```ruby
67
+ KFUNC_PROBE :vfs_read do
68
+ bpf_trace_printk("Reading file: %d\n", args.got_bits)
69
+ 0
70
+ end
71
+ ```
72
+
73
+ **Generated C:**
74
+ ```c
75
+ KFUNC_PROBE(vfs_read) {
76
+ bpf_trace_printk("Reading file: %d\n", args->got_bits);
77
+ return 0;
78
+ }
79
+ ```
80
+
81
+ ### Helper Functions
82
+
83
+ **Ruby DSL:**
84
+ ```ruby
85
+ def print_event(_ctx)
86
+ bpf_trace_printk("Event occurred\n")
87
+ 0
88
+ end
89
+ ```
90
+
91
+ **Generated C:**
92
+ ```c
93
+ int print_event(void *_ctx) {
94
+ bpf_trace_printk("Event occurred\n");
95
+ return 0;
96
+ }
97
+ ```
98
+
99
+ ## Usage
100
+
101
+ ```ruby
102
+ require 'rubita'
103
+
104
+ ruby_code = <<~RUBY
105
+ BPF_HASH :counts, key: :u64, value: :u64, size: 10
106
+
107
+ TRACEPOINT_PROBE :syscalls, :sys_enter_openat do
108
+ bpf_trace_printk("openat\n")
109
+ 0
110
+ end
111
+ RUBY
112
+
113
+ c_code = Rubita.transpile(ruby_code)
114
+ puts c_code
115
+ ```
116
+
117
+ ## Development
118
+
119
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
120
+
121
+ ## Contributing
122
+
123
+ Bug reports and pull requests are welcome on GitHub at https://github.com/udzura/rubita.
124
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,321 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubita
4
+ class Transpiler
5
+ def transpile(source)
6
+ sexp = Ripper.sexp(source)
7
+ raise Error, "failed to parse source" if sexp.nil?
8
+
9
+ nodes = extract_program_nodes(sexp)
10
+
11
+ converted = nodes.map do |node|
12
+ case node[0]
13
+ when :def
14
+ convert_definition(node)
15
+ when :command
16
+ convert_top_level_command(node)
17
+ when :method_add_block
18
+ convert_top_level_block(node)
19
+ else
20
+ raise Error, "unsupported top-level node: #{node[0]}"
21
+ end
22
+ end
23
+
24
+ converted.join("\n\n")
25
+ end
26
+
27
+ private
28
+
29
+ def extract_program_nodes(sexp)
30
+ return raise Error, "unexpected program structure" unless sexp[0] == :program
31
+
32
+ nodes = sexp[1]
33
+ return raise Error, "source must contain nodes" unless nodes.is_a?(Array) && !nodes.empty?
34
+
35
+ nodes
36
+ end
37
+
38
+ def convert_top_level_command(node)
39
+ ident = node[1]
40
+ return raise Error, "unsupported command format" unless [:@ident, :@const].include?(ident&.[](0))
41
+
42
+ case ident[1]
43
+ when "BPF_HASH"
44
+ convert_hashmap_command(node)
45
+ else
46
+ raise Error, "unsupported command: #{ident[1]}"
47
+ end
48
+ end
49
+
50
+ def convert_top_level_block(node)
51
+ call_node = node[1]
52
+ block_node = node[2]
53
+
54
+ return raise Error, "unsupported block call" unless call_node&.[](0) == :command
55
+
56
+ ident = call_node[1]
57
+ return raise Error, "unsupported block command format" unless ident&.[](0) == :@const
58
+
59
+ case ident[1]
60
+ when "TRACEPOINT_PROBE"
61
+ convert_probe_block(call_node, block_node, "TRACEPOINT_PROBE", 2)
62
+ when "KFUNC_PROBE"
63
+ convert_probe_block(call_node, block_node, "KFUNC_PROBE", 1)
64
+ when "KRETFUNC_PROBE"
65
+ convert_probe_block(call_node, block_node, "KRETFUNC_PROBE", 1)
66
+ when "LSM_PROBE"
67
+ convert_probe_block(call_node, block_node, "LSM_PROBE", 1)
68
+ else
69
+ raise Error, "unsupported block command: #{ident[1]}"
70
+ end
71
+ end
72
+
73
+ def convert_probe_block(call_node, block_node, macro_name, arg_count)
74
+ args_add_block = call_node[2]
75
+ return raise Error, "unsupported #{macro_name} args" unless args_add_block&.[](0) == :args_add_block
76
+
77
+ raw_args = args_add_block[1]
78
+ return raise Error, "#{macro_name} requires #{arg_count} arguments" unless raw_args.is_a?(Array) && raw_args.size == arg_count
79
+
80
+ macro_args = raw_args.map { |arg| convert_symbol_literal(arg) }
81
+
82
+ return raise Error, "#{macro_name} requires do ... end block" unless block_node&.[](0) == :do_block
83
+ bodystmt = block_node[2]
84
+ statements, return_value = extract_body_statements_and_return_from_bodystmt(bodystmt)
85
+
86
+ c_lines = ["#{macro_name}(#{macro_args.join(', ')}) {"]
87
+ statements.each do |statement|
88
+ c_lines << " #{convert_statement(statement)}"
89
+ end
90
+ c_lines << " return #{return_value};"
91
+ c_lines << "}"
92
+ c_lines.join("\n")
93
+ end
94
+
95
+ def convert_hashmap_command(node)
96
+ args_add_block = node[2]
97
+ return raise Error, "unsupported hashmap args" unless args_add_block[0] == :args_add_block
98
+
99
+ raw_args = args_add_block[1]
100
+ map_name = convert_symbol_literal(raw_args[0])
101
+
102
+ options_node = raw_args[1]
103
+ return raise Error, "hashmap options are required" unless options_node&.[](0) == :bare_assoc_hash
104
+
105
+ options = extract_assoc_hash(options_node)
106
+ key_type = options["key"] || (raise Error, "hashmap key is required")
107
+ value_type = options["value"] || (raise Error, "hashmap value is required")
108
+ size = options["size"]
109
+
110
+ if size
111
+ "BPF_HASH(#{map_name}, #{key_type}, #{value_type}, #{size});"
112
+ else
113
+ "BPF_HASH(#{map_name}, #{key_type}, #{value_type});"
114
+ end
115
+ end
116
+
117
+ def extract_assoc_hash(node)
118
+ pairs = node[1]
119
+ return raise Error, "invalid hashmap options" unless pairs.is_a?(Array)
120
+
121
+ pairs.each_with_object({}) do |pair, acc|
122
+ return raise Error, "invalid hashmap option pair" unless pair[0] == :assoc_new
123
+
124
+ label_node = pair[1]
125
+ value_node = pair[2]
126
+ return raise Error, "invalid hashmap option key" unless label_node[0] == :@label
127
+
128
+ key = label_node[1].delete_suffix(":")
129
+ value = convert_hashmap_option_value(value_node)
130
+ acc[key] = value
131
+ end
132
+ end
133
+
134
+ def convert_hashmap_option_value(node)
135
+ case node[0]
136
+ when :symbol_literal
137
+ convert_symbol_literal(node)
138
+ when :@int
139
+ node[1]
140
+ else
141
+ raise Error, "unsupported hashmap option value: #{node[0]}"
142
+ end
143
+ end
144
+
145
+ def convert_symbol_literal(node)
146
+ symbol_ident = node.dig(1, 1)
147
+ return raise Error, "unsupported symbol literal" unless symbol_ident&.[](0) == :@ident
148
+
149
+ symbol_ident[1]
150
+ end
151
+
152
+ def convert_definition(def_node)
153
+ function_name = extract_function_name(def_node)
154
+ statements, return_value = extract_body_statements_and_return(def_node)
155
+
156
+ c_lines = ["int #{function_name}(void *_ctx) {"]
157
+ statements.each do |statement|
158
+ c_lines << " #{convert_statement(statement)}"
159
+ end
160
+ c_lines << " return #{return_value};"
161
+ c_lines << "}"
162
+ c_lines.join("\n")
163
+ end
164
+
165
+ def extract_function_name(def_node)
166
+ ident = def_node[1]
167
+ return raise Error, "method name is missing" unless ident&.[](0) == :@ident
168
+
169
+ ident[1]
170
+ end
171
+
172
+ def extract_body_statements_and_return(def_node)
173
+ bodystmt = def_node[3]
174
+ extract_body_statements_and_return_from_bodystmt(bodystmt)
175
+ end
176
+
177
+ def extract_body_statements_and_return_from_bodystmt(bodystmt)
178
+ stmts = bodystmt[1]
179
+ return raise Error, "method body is missing" unless stmts.is_a?(Array) && !stmts.empty?
180
+
181
+ return_node = stmts.last
182
+ return raise Error, "last expression must be integer literal" unless return_node[0] == :@int
183
+
184
+ [stmts[0...-1], return_node[1]]
185
+ end
186
+
187
+ def convert_statement(statement)
188
+ case statement[0]
189
+ when :method_add_arg
190
+ convert_method_call_statement(statement)
191
+ else
192
+ raise Error, "unsupported statement: #{statement[0]}"
193
+ end
194
+ end
195
+
196
+ def convert_method_call_statement(statement)
197
+ call_target = statement[1]
198
+ arg_part = statement[2]
199
+
200
+ case call_target[0]
201
+ when :fcall
202
+ convert_fcall_statement(call_target, arg_part)
203
+ when :call
204
+ convert_call_statement(call_target, arg_part)
205
+ else
206
+ raise Error, "unsupported call target type: #{call_target[0]}"
207
+ end
208
+ end
209
+
210
+ def convert_fcall_statement(call_target, arg_part)
211
+ method_ident = call_target[1]
212
+ return raise Error, "unsupported method identifier" unless method_ident[0] == :@ident
213
+
214
+ args = extract_args(arg_part)
215
+ "#{method_ident[1]}(#{args.join(', ')});"
216
+ end
217
+
218
+ def convert_call_statement(call_target, arg_part)
219
+ receiver = call_target[1]
220
+ method_name_node = call_target[3]
221
+
222
+ return raise Error, "unsupported call method name" unless method_name_node&.[](0) == :@ident
223
+
224
+ method_name = method_name_node[1]
225
+
226
+ # Check if receiver is a global variable
227
+ if receiver&.[](0) == :var_ref && receiver[1]&.[](0) == :@gvar
228
+ gvar_name = receiver[1][1].delete_prefix("$")
229
+ args = extract_args_with_reference(arg_part)
230
+ "#{gvar_name}.#{method_name}(#{args.join(', ')});"
231
+ else
232
+ raise Error, "unsupported call receiver type: #{receiver&.[](0)}"
233
+ end
234
+ end
235
+
236
+ def convert_method_call(statement)
237
+ call_target = statement[1]
238
+ arg_part = statement[2]
239
+
240
+ return raise Error, "unsupported call target" unless call_target[0] == :fcall
241
+ method_ident = call_target[1]
242
+ return raise Error, "unsupported method identifier" unless method_ident[0] == :@ident
243
+
244
+ args = extract_args(arg_part)
245
+ "#{method_ident[1]}(#{args.join(', ')});"
246
+ end
247
+
248
+ def extract_args_with_reference(arg_part)
249
+ return raise Error, "unsupported arg format" unless arg_part[0] == :arg_paren
250
+
251
+ args_add_block = arg_part[1]
252
+ return raise Error, "unsupported arg list" unless args_add_block[0] == :args_add_block
253
+
254
+ raw_args = args_add_block[1]
255
+ raw_args.map { |arg| convert_arg_with_reference(arg) }
256
+ end
257
+
258
+ def convert_arg_with_reference(arg)
259
+ case arg[0]
260
+ when :vcall
261
+ arg_ident = arg[1]
262
+ return raise Error, "unsupported variable call" unless arg_ident&.[](0) == :@ident
263
+ "&#{arg_ident[1]}"
264
+ when :string_literal
265
+ string_content = arg.dig(1, 1)
266
+ return raise Error, "unsupported string format" unless string_content&.[](0) == :@tstring_content
267
+
268
+ escaped = escape_c_string(string_content[1])
269
+ %Q("#{escaped}")
270
+ when :call
271
+ convert_call_arg(arg)
272
+ else
273
+ raise Error, "unsupported argument type for reference: #{arg[0]}"
274
+ end
275
+ end
276
+
277
+ def extract_args(arg_part)
278
+ return raise Error, "unsupported arg format" unless arg_part[0] == :arg_paren
279
+
280
+ args_add_block = arg_part[1]
281
+ return raise Error, "unsupported arg list" unless args_add_block[0] == :args_add_block
282
+
283
+ raw_args = args_add_block[1]
284
+ raw_args.map { |arg| convert_arg(arg) }
285
+ end
286
+
287
+ def convert_arg(arg)
288
+ case arg[0]
289
+ when :string_literal
290
+ string_content = arg.dig(1, 1)
291
+ return raise Error, "unsupported string format" unless string_content&.[](0) == :@tstring_content
292
+
293
+ escaped = escape_c_string(string_content[1])
294
+ %Q("#{escaped}")
295
+ when :call
296
+ convert_call_arg(arg)
297
+ else
298
+ raise Error, "unsupported argument type: #{arg[0]}"
299
+ end
300
+ end
301
+
302
+ def convert_call_arg(call_node)
303
+ receiver = call_node[1]
304
+ method_name_node = call_node[3]
305
+
306
+ return raise Error, "unsupported call receiver" unless receiver&.[](0) == :vcall
307
+ receiver_ident = receiver[1]
308
+ return raise Error, "unsupported receiver identifier" unless receiver_ident&.[](0) == :@ident
309
+
310
+ return raise Error, "unsupported method name" unless method_name_node&.[](0) == :@ident
311
+
312
+ receiver_name = receiver_ident[1]
313
+ method_name = method_name_node[1]
314
+ "#{receiver_name}->#{method_name}"
315
+ end
316
+
317
+ def escape_c_string(value)
318
+ value.gsub("\\", "\\\\").gsub('"', '\\"')
319
+ end
320
+ end
321
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubita
4
+ VERSION = "0.1.0"
5
+ end
data/lib/rubita.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+
5
+ require_relative "rubita/version"
6
+ require_relative "rubita/transpiler"
7
+
8
+ module Rubita
9
+ class Error < StandardError; end
10
+
11
+ def self.transpile(source)
12
+ Transpiler.new.transpile(source)
13
+ end
14
+ end
data/sig/rubita.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Rubita
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubita
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Uchio Kondo
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Rubita transpiles a restricted Ruby DSL into BCC-compatible C code for
13
+ eBPF use cases.
14
+ email:
15
+ - udzura@udzura.jp
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - Rakefile
22
+ - lib/rubita.rb
23
+ - lib/rubita/transpiler.rb
24
+ - lib/rubita/version.rb
25
+ - sig/rubita.rbs
26
+ homepage: https://github.com/udzura/rubita
27
+ licenses: []
28
+ metadata:
29
+ allowed_push_host: https://rubygems.org
30
+ homepage_uri: https://github.com/udzura/rubita
31
+ source_code_uri: https://github.com/udzura/rubita
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 3.2.0
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubygems_version: 4.0.6
47
+ specification_version: 4
48
+ summary: Ruby to BCC-compatible C transpiler for eBPF programs
49
+ test_files: []