Skip to content

Commit 8f3c455

Browse files
committed
Add first-class perf attachments with generic read support
Introduce PerfAttachment as the return value for perf_event attach calls, allowing multiple perf attachments to coexist for the same loaded program. Lower read(attachment) to attachment-specific perf counter reads and support detach(attachment) separately from detach(program). Update perf attach bookkeeping to track attachment ids, preserve per-attachment perf fds, and emit more specific attach/detach status messages including the event config. Also add skeleton cleanup registration so generated userspace programs destroy the libbpf skeleton on exit. Refresh docs, examples, and tests for the new read()/PerfAttachment API.
1 parent 6c288f3 commit 8f3c455

14 files changed

Lines changed: 419 additions & 175 deletions

BUILTINS.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ fn main() -> i32 {
8585

8686
#### `attach(handle, target, flags)` / `attach(handle, opts, flags)`
8787
**Signature:** `attach(handle: ProgramHandle, target: str(128), flags: u32) -> u32`
88-
**Signature:** `attach(handle: ProgramHandle, opts: perf_options, flags: u32) -> u32`
88+
**Signature:** `attach(handle: ProgramHandle, opts: perf_options, flags: u32) -> PerfAttachment`
8989
**Variadic:** No
9090
**Context:** Userspace only
9191

@@ -102,8 +102,8 @@ fn main() -> i32 {
102102
- `flags`: Reserved (pass `0`)
103103

104104
**Return Value:**
105-
- Returns `0` on success
106-
- Returns error code on failure
105+
- Standard form returns `0` on success and an error code on failure
106+
- Perf event form returns a `PerfAttachment` value with the open counter/link identity
107107

108108
**Examples:**
109109
```kernelscript
@@ -116,7 +116,9 @@ if (result != 0) {
116116
// Minimal perf attach — all non-perf_type/perf_config fields use defaults:
117117
// pid=-1 (all procs), cpu=0, period=1_000_000, wakeup=1, flags=false
118118
var perf_prog = load(on_branch_miss)
119-
attach(perf_prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses }, 0)
119+
var perf_att = attach(perf_prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses }, 0)
120+
var count = read(perf_att)
121+
detach(perf_att)
120122
detach(perf_prog)
121123
```
122124

@@ -129,13 +131,14 @@ detach(perf_prog)
129131

130132
#### `detach(handle)`
131133
**Signature:** `detach(handle: ProgramHandle) -> void`
134+
**Signature:** `detach(handle: PerfAttachment) -> void`
132135
**Variadic:** No
133136
**Context:** Userspace only
134137

135-
**Description:** Detach a loaded eBPF program from its current attachment point.
138+
**Description:** Detach a loaded eBPF program from its current attachment point, or tear down one perf attachment.
136139

137140
**Parameters:**
138-
- `handle`: Program handle returned from `load()`
141+
- `handle`: Program handle returned from `load()`, or a `PerfAttachment` returned from perf `attach()`
139142

140143
**Return Value:**
141144
- No return value (void)
@@ -150,11 +153,27 @@ detach(prog) // Clean up
150153

151154
**Context-specific implementations:**
152155
- **eBPF:** Not available
153-
- **Userspace:** Uses `detach_bpf_program_by_fd` function
156+
- **Userspace:** Uses `detach_bpf_program_by_fd` for program handles and `ks_detach_perf_attachment` for perf attachments
154157
- **Kernel Module:** Not available
155158

156159
---
157160

161+
#### `read(handle)`
162+
**Signature:** `read(handle: PerfAttachment) -> i64`
163+
**Variadic:** No
164+
**Context:** Userspace only
165+
166+
**Description:** Read the current hardware/software counter value from a perf attachment.
167+
168+
**Parameters:**
169+
- `handle`: Perf attachment returned from `attach(handle, perf_options, flags)`
170+
171+
**Return Value:**
172+
- Returns the raw 64-bit counter value on success
173+
- Returns `-1` on error
174+
175+
---
176+
158177
### 3. Struct Operations (struct_ops)
159178

160179
#### `register(impl_instance)`
@@ -405,4 +424,4 @@ if (result != 0) {
405424
## See Also
406425

407426
- **SPEC.md**: Language specification and features
408-
- **examples/**: Example programs demonstrating builtin function usage
427+
- **examples/**: Example programs demonstrating builtin function usage

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ fn main() -> i32 {
270270

271271
### Hardware Performance Counter Programs
272272

273-
Use `@perf_event` to attach eBPF programs to hardware or software performance counters. `perf_options` keeps the kernel's tagged `perf_type + perf_config` model, so adding new perf event families does not require flattening everything into one enum. Only `perf_type` and `perf_config` are required; all other fields have sensible defaults. If you need the current count in userspace, call `perf_read(prog)` after `attach(...)`:
273+
Use `@perf_event` to attach eBPF programs to hardware or software performance counters. `perf_options` keeps the kernel's tagged `perf_type + perf_config` model, so adding new perf event families does not require flattening everything into one enum. Only `perf_type` and `perf_config` are required; all other fields have sensible defaults. Perf attaches return a first-class attachment value, so if you need the current count in userspace, call `read(att)`:
274274

275275
```kernelscript
276276
// eBPF program fires on every hardware branch-miss sample
@@ -284,11 +284,12 @@ fn main() -> i32 {
284284
285285
// Minimal form — defaults: pid=-1 (all procs), cpu=0,
286286
// period=1_000_000, wakeup=1, all flags=false
287-
attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses }, 0)
288-
var count = perf_read(prog)
287+
var att = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses }, 0)
288+
var count = read(att)
289289
print("branch misses: %lld", count)
290290
291-
detach(prog) // disables counter, destroys BPF link, closes fd
291+
detach(att) // disables counter, destroys BPF link, closes fd
292+
detach(prog) // safe cleanup for the loaded program handle
292293
return 0
293294
}
294295
```

SPEC.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -462,21 +462,22 @@ fn main() -> i32 {
462462
463463
// Only perf_type + perf_config are required; all other fields use language-level defaults:
464464
// pid=-1, cpu=0, period=1_000_000, wakeup=1, inherit/exclude_*=false
465-
attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses }, 0)
465+
var misses = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses }, 0)
466466
467467
// Override specific fields as needed:
468-
attach(prog, perf_options {
468+
var cache = attach(prog, perf_options {
469469
perf_type: perf_type_hardware,
470470
perf_config: cache_misses,
471471
cpu: 2,
472472
period: 500000,
473473
exclude_kernel: true,
474474
}, 0)
475475
476-
var count = perf_read(prog)
477-
print("count: %lld", count)
476+
print("misses=%lld cache=%lld", read(misses), read(cache))
478477
479-
detach(prog) // IOC_DISABLE → bpf_link__destroy → close(perf_fd)
478+
detach(cache) // IOC_DISABLE → bpf_link__destroy → close(perf_fd)
479+
detach(misses)
480+
detach(prog)
480481
return 0
481482
}
482483
```
@@ -536,9 +537,9 @@ For event families with a richer config space, such as `perf_type_hw_cache`, pro
536537
| Function | Signature | Description |
537538
|---|---|---|
538539
| `ks_open_perf_event` | `int (ks_perf_options)` | Calls `perf_event_open(2)`, returns fd |
539-
| `ks_attach_perf_event` | `int (int prog_fd, ks_perf_options, int flags)` | Full open-reset-attach-enable lifecycle |
540+
| `ks_attach_perf_event` | `PerfAttachment (int prog_fd, ks_perf_options, int flags)` | Full open-reset-attach-enable lifecycle |
540541
| `ks_read_perf_count` | `int64_t (int perf_fd)` | Reads current 64-bit counter via `read()` |
541-
| `ks_perf_read` | `int64_t (int prog_fd)` | High-level read via program handle |
542+
| `ks_perf_attachment_read` | `int64_t (PerfAttachment)` | High-level read via attachment value |
542543

543544
**Attach sequence (compiler-generated, inside `ks_attach_perf_event`):**
544545
1. `ks_attr.attr.disabled = 1` — open counter without starting it
@@ -554,6 +555,7 @@ For event families with a richer config space, such as `perf_type_hw_cache`, pro
554555

555556
**Compiler implementation:**
556557
- Detects `attach(prog, perf_options_value, flags)` (three-argument form with `perf_options` second arg) and routes to `ks_attach_perf_event`
558+
- Returns a first-class `PerfAttachment` value for perf attaches so one program can hold multiple live counters
557559
- Exposes omitted `perf_options` fields as language-level defaults (partial struct literal)
558560
- Validates `pid ≥ -1`, `cpu ≥ -1`, and rejects `pid == -1 && cpu == -1` at runtime
559561
- Emits `PERF_FLAG_FD_CLOEXEC` for safe fd inheritance

examples/perf_cache_miss.ks

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ fn main() -> i32 {
1414
// Only perf_type + perf_config are required; pid, cpu, period, wakeup and flag fields
1515
// default to: pid=-1 (all procs), cpu=0, period=1_000_000, wakeup=1,
1616
// inherit/exclude_kernel/exclude_user=false.
17-
attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: cache_misses, period: 10000000, inherit: true }, 0)
18-
print("Cache-miss perf_event demo attached")
19-
var count = perf_read(prog)
20-
print("Cache-miss count: %lld", count)
17+
var cache = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: cache_misses, period: 10000000, inherit: true }, 0)
18+
var branch = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses, period: 10000000, inherit: true }, 0)
19+
print("Cache-miss and branch-miss perf_event demo attached")
20+
var cache_count = read(cache)
21+
print("Cache-miss count: %lld", cache_count)
22+
var branch_count = read(branch)
23+
print("Branch-miss count: %lld", branch_count)
2124

25+
detach(cache)
26+
detach(branch)
2227
detach(prog)
23-
print("Cache-miss perf_event demo detached")
28+
print("Cache-miss and branch-miss perf_event demo detached")
2429
return 0
2530
}

examples/perf_page_fault.ks

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ fn main() -> i32 {
1414
// pid: 0 = current process, cpu: -1 = any CPU (standard per-process monitoring).
1515
// page_faults (PERF_COUNT_SW_PAGE_FAULTS) is the most reliable software event:
1616
// every heap/stack allocation triggers minor page faults, no scheduler dependency.
17-
attach(prog, perf_options { perf_type: perf_type_software, perf_config: page_faults, pid: 0, cpu: -1, period: 1 }, 0)
17+
var att = attach(prog, perf_options { perf_type: perf_type_software, perf_config: page_faults, pid: 0, cpu: -1, period: 1 }, 0)
1818
print("Page-fault perf_event demo attached")
1919

2020
// Repeatedly increment a counter; stack/heap activity will generate page faults.
@@ -23,10 +23,11 @@ fn main() -> i32 {
2323
x = x + 1
2424
}
2525

26-
var count = perf_read(prog)
26+
var count = read(att)
2727
print("Page-fault count: %lld", count)
2828

29-
detach(prog)
29+
detach(att)
3030
print("Page-fault perf_event demo detached")
31+
detach(prog)
3132
return 0
3233
}

src/btf_parser.ml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,8 +522,9 @@ fn main() -> i32 {
522522
var prog = load(%s)
523523

524524
// perf_type + perf_config are required; all other fields default to sensible values.
525-
attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses }, 0)
525+
var att = attach(prog, perf_options { perf_type: perf_type_hardware, perf_config: branch_misses }, 0)
526526

527+
detach(att)
527528
detach(prog)
528529

529530
return 0

src/stdlib.ml

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,24 @@ let validate_attach_function arg_types _ast_context _pos =
121121
| _ ->
122122
(false, Some "attach() requires (handle, target, flags) — target is a string or perf_options { ... }")
123123

124+
(** Validation function for read() - currently only accepts perf attachment values *)
125+
let validate_read_function arg_types _ast_context _pos =
126+
match arg_types with
127+
| [Struct "PerfAttachment"] | [UserType "PerfAttachment"] ->
128+
(true, None)
129+
| _ ->
130+
(false, Some "read() currently requires a PerfAttachment")
131+
132+
(** Validation function for detach() - accepts program handles and perf attachments *)
133+
let validate_detach_function arg_types _ast_context _pos =
134+
match arg_types with
135+
| [ProgramHandle]
136+
| [Struct "PerfAttachment"]
137+
| [UserType "PerfAttachment"] ->
138+
(true, None)
139+
| _ ->
140+
(false, Some "detach() requires a ProgramHandle or PerfAttachment")
141+
124142
(** Standard library built-in functions *)
125143
let builtin_functions = [
126144
{
@@ -158,14 +176,14 @@ let builtin_functions = [
158176
};
159177
{
160178
name = "detach";
161-
param_types = [ProgramHandle]; (* program handle only *)
179+
param_types = []; (* Custom validation handles program handles and perf attachments *)
162180
return_type = Void; (* void - no return value *)
163-
description = "Detach a loaded eBPF program from its current attachment";
181+
description = "Detach a loaded eBPF program or perf attachment from its current attachment";
164182
is_variadic = false;
165183
ebpf_impl = ""; (* Not available in eBPF context *)
166184
userspace_impl = "detach_bpf_program_by_fd";
167185
kernel_impl = "";
168-
validate = None;
186+
validate = Some validate_detach_function;
169187
};
170188
{
171189
name = "register";
@@ -223,15 +241,15 @@ let builtin_functions = [
223241
validate = Some validate_exec_function;
224242
};
225243
{
226-
name = "perf_read";
227-
param_types = [ProgramHandle];
244+
name = "read";
245+
param_types = []; (* Custom validation handles attachment-aware overloads *)
228246
return_type = I64; (* Raw counter value, or -1 on error *)
229-
description = "Read the current hardware/software counter value for a perf_event program";
247+
description = "Read the current hardware/software counter value for a perf attachment";
230248
is_variadic = false;
231249
ebpf_impl = ""; (* Not available in eBPF context *)
232-
userspace_impl = "ks_perf_read";
250+
userspace_impl = "ks_perf_attachment_read";
233251
kernel_impl = "";
234-
validate = None;
252+
validate = Some validate_read_function;
235253
};
236254
]
237255

@@ -337,6 +355,13 @@ let builtin_types = [
337355
("exclude_kernel", Bool);
338356
("exclude_user", Bool);
339357
], builtin_pos));
358+
359+
(* PerfAttachment: first-class userspace handle returned by perf_event attach(). *)
360+
TypeDef (StructDef ("PerfAttachment", [
361+
("perf_fd", I32);
362+
("link_id", I32);
363+
("prog_fd", I32);
364+
], builtin_pos));
340365
]
341366

342367
(** Default field values for structs that support partial initialisation.
@@ -392,4 +417,4 @@ let format_function_args context_type args =
392417
(* For userspace, printf can handle more flexible formatting *)
393418
(match args with
394419
| [] -> ["\"\\n\""] (* Empty print with newline *)
395-
| _ -> args) (* Pass arguments as-is *)
420+
| _ -> args) (* Pass arguments as-is *)

src/type_checker.ml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ let rec unify_types t1 t2 =
321321
(match unify_types t1 t2 with
322322
| Some unified -> Some (Pointer unified)
323323
| None -> None)
324+
325+
(* Named structs and user types unify when they refer to the same type name. *)
326+
| Struct name1, UserType name2
327+
| UserType name1, Struct name2 when name1 = name2 ->
328+
Some (Struct name1)
324329

325330
(* Result types *)
326331
| Result (ok1, err1), Result (ok2, err2) ->
@@ -394,6 +399,17 @@ let can_assign to_type from_type =
394399
| Some _ -> true
395400
| None -> false))
396401

402+
let builtin_return_type_for_call name arg_types default_return_type =
403+
match name, arg_types with
404+
| "attach", [ProgramHandle; (Struct "perf_options" | UserType "perf_options"); _] ->
405+
Struct "PerfAttachment"
406+
| "detach", _ ->
407+
Void
408+
| "read", _ ->
409+
I64
410+
| _ ->
411+
default_return_type
412+
397413

398414

399415
(** Helper function to get the type of a literal *)
@@ -642,7 +658,8 @@ let type_check_builtin_call ctx name typed_args arg_types pos =
642658
| None -> type_error ("Validation failed for function: " ^ name) pos)
643659
else
644660
(* Validation passed - accept any number of arguments *)
645-
Some { texpr_desc = TCall (make_typed_identifier name pos, typed_args); texpr_type = return_type; texpr_pos = pos }
661+
let actual_return_type = builtin_return_type_for_call name arg_types return_type in
662+
Some { texpr_desc = TCall (make_typed_identifier name pos, typed_args); texpr_type = actual_return_type; texpr_pos = pos }
646663
| Some _ ->
647664
(* Check if this function has custom validation *)
648665
let (validation_ok, validation_error) = Stdlib.validate_builtin_call name arg_types ctx.ast_context pos in
@@ -655,11 +672,13 @@ let type_check_builtin_call ctx name typed_args arg_types pos =
655672
(* Skip standard type checking if param_types is empty (custom validation handles it) *)
656673
if List.length expected_params = 0 then
657674
(* Custom validation handled type checking *)
658-
Some { texpr_desc = TCall (make_typed_identifier name pos, typed_args); texpr_type = return_type; texpr_pos = pos }
675+
let actual_return_type = builtin_return_type_for_call name arg_types return_type in
676+
Some { texpr_desc = TCall (make_typed_identifier name pos, typed_args); texpr_type = actual_return_type; texpr_pos = pos }
659677
else if List.length expected_params = List.length arg_types then
660678
let unified = List.map2 unify_types expected_params arg_types in
661679
if List.for_all (function Some _ -> true | None -> false) unified then
662-
Some { texpr_desc = TCall (make_typed_identifier name pos, typed_args); texpr_type = return_type; texpr_pos = pos }
680+
let actual_return_type = builtin_return_type_for_call name arg_types return_type in
681+
Some { texpr_desc = TCall (make_typed_identifier name pos, typed_args); texpr_type = actual_return_type; texpr_pos = pos }
663682
else
664683
type_error ("Type mismatch in function call: " ^ name) pos
665684
else
@@ -3463,4 +3482,3 @@ and populate_multi_program_context ast multi_prog_analysis =
34633482
) ast
34643483

34653484

3466-

0 commit comments

Comments
 (0)