Skip to content

Commit 4ea0b16

Browse files
committed
ui: harden modal widget user_data handling
Replace function-pointer-in-userdata pattern with a data context struct.
1 parent 7e98423 commit 4ea0b16

File tree

2 files changed

+58
-14
lines changed

2 files changed

+58
-14
lines changed

src/ui/ui.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub const InitOpt = struct {
3232
pub fn init(opt: InitOpt) !void {
3333
allocator = opt.allocator;
3434
settings.allocator = opt.allocator;
35+
widget.allocator = opt.allocator;
3536
lvgl.init();
3637
const disp = try drv.initDisplay();
3738
drv.initInput() catch |err| {

src/ui/widget.zig

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ const lvgl = @import("lvgl.zig");
33

44
const logger = std.log.scoped(.ui);
55

6+
// NOTE: ui.init() must set this (e.g. widget.allocator = opt.allocator).
7+
pub var allocator: std.mem.Allocator = undefined;
8+
69
// defined in ui.c
710
extern fn nm_keyboard_popon(input: *lvgl.LvObj) void;
811
extern fn nm_keyboard_popoff() void;
@@ -60,18 +63,34 @@ pub fn topdrop(onoff: enum { show, remove }) void {
6063
/// provided as btns arg to modal.
6164
pub const ModalButtonCallbackFn = *const fn (index: usize) void;
6265

66+
/// Context stored in the modal window's user data.
67+
/// We intentionally store a *data pointer* in LVGL user_data (void*), never a function
68+
/// pointer, because data-pointer <-> function-pointer casts are not portable and can
69+
/// break across Zig versions / ABIs.
70+
///
71+
/// We also store the allocator used to allocate this context so the delete handler
72+
/// can always free with the same allocator.
73+
const ModalCtx = struct {
74+
alloc: std.mem.Allocator,
75+
cb: ModalButtonCallbackFn,
76+
};
77+
6378
/// shows a non-dismissible window using the whole screen real estate;
6479
/// for use in place of lv_msgbox_create.
6580
///
6681
/// while all heap-alloc'ed resources are free'd automatically right before cb is called,
6782
/// the value of title, text and btns args must live at least as long as cb; they are
6883
/// memory-managed by the callers.
69-
///
70-
/// note: the cb callback must have @alignOf(ModalbuttonCallbackFn) alignment.
7184
pub fn modal(title: [*:0]const u8, text: [*:0]const u8, btns: []const [*:0]const u8, cb: ModalButtonCallbackFn) !void {
7285
const win = try lvgl.Window.newTop(60, title);
7386
errdefer win.destroy(); // also deletes all children created below
74-
win.setUserdata(cb);
87+
88+
const ctx = try allocator.create(ModalCtx);
89+
ctx.* = .{ .alloc = allocator, .cb = cb };
90+
win.setUserdata(ctx);
91+
92+
// Free context when the window is deleted, regardless of deletion path.
93+
_ = win.on(.delete, nm_modal_delete_callback, null);
7594

7695
const wincont = win.content().flex(.column, .{ .cross = .center, .track = .center });
7796
const msg = try lvgl.Label.new(wincont, text, .{ .pos = .center });
@@ -89,7 +108,10 @@ pub fn modal(title: [*:0]const u8, text: [*:0]const u8, btns: []const [*:0]const
89108
const btn = try lvgl.TextButton.new(btncont, btext);
90109
btn.setFlag(.event_bubble);
91110
btn.setFlag(.user1); // .user1 indicates actionable button in callback
92-
btn.setUserdata(@ptrFromInt(i)); // button index in callback
111+
112+
// Store (i+1) so userdata is never null (0). In callback: idx = intFromPtr - 1.
113+
btn.setUserdata(@ptrFromInt(i + 1));
114+
93115
btn.setWidth(btnwidth);
94116
if (i == 0) {
95117
btn.addStyle(lvgl.nm_style_btn_red(), .{});
@@ -98,17 +120,38 @@ pub fn modal(title: [*:0]const u8, text: [*:0]const u8, btns: []const [*:0]const
98120
_ = btncont.on(.click, nm_modal_callback, win.lvobj);
99121
}
100122

123+
/// Frees modal context when the window is deleted. This runs for all deletion paths
124+
/// (button click, screen unload, parent cleanup, etc.) and avoids leaks/double-frees.
125+
export fn nm_modal_delete_callback(e: *lvgl.LvEvent) callconv(.C) void {
126+
const win = lvgl.Window{ .lvobj = e.target() };
127+
128+
const ctx_any = win.userdata() orelse return;
129+
const ctx: *ModalCtx = @ptrCast(@alignCast(ctx_any));
130+
131+
// Clear userdata so any accidental future access becomes a null-check failure.
132+
win.setUserdata(null);
133+
134+
ctx.alloc.destroy(ctx);
135+
}
136+
101137
export fn nm_modal_callback(e: *lvgl.LvEvent) callconv(.C) void {
102-
if (e.userdata()) |edata| {
103-
const target = lvgl.Container{ .lvobj = e.target() }; // type doesn't really matter
104-
if (!target.hasFlag(.user1)) { // .user1 is set in modal setup
105-
return;
106-
}
138+
const edata = e.userdata() orelse return;
107139

108-
const btn_index = @intFromPtr(target.userdata());
109-
const win = lvgl.Window{ .lvobj = @ptrCast(edata) };
110-
const cb: ModalButtonCallbackFn = @alignCast(@ptrCast(win.userdata()));
111-
win.destroy();
112-
cb(btn_index);
140+
const target = lvgl.Container{ .lvobj = e.target() }; // type doesn't really matter
141+
if (!target.hasFlag(.user1)) { // .user1 is set in modal setup
142+
return;
113143
}
144+
145+
const idx_any = target.userdata() orelse return;
146+
const btn_index: usize = @intFromPtr(idx_any) - 1;
147+
148+
const win = lvgl.Window{ .lvobj = @ptrCast(edata) };
149+
const ctx_any = win.userdata() orelse return;
150+
const ctx: *ModalCtx = @ptrCast(@alignCast(ctx_any));
151+
const cb = ctx.cb;
152+
153+
// Destroying the window triggers LV_EVENT_DELETE, which frees ctx.
154+
win.destroy();
155+
156+
cb(btn_index);
114157
}

0 commit comments

Comments
 (0)