Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/perry-ui-windows/src/ffi/widget_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ pub extern "C" fn perry_ui_text_create(text_ptr: i64) -> i64 {
widgets::text::create(text_ptr as *const u8)
}

/// Create an AdBanner placeholder (#867). `unit_id_ptr` / `size_ptr` are
/// string pointers per the `AdBanner(unitId, size)` dispatch row. This
/// symbol was previously absent from the Windows staticlib, so any TS
/// program using AdBanner failed at link time.
#[no_mangle]
pub extern "C" fn perry_ui_adbanner_create(unit_id_ptr: i64, size_ptr: i64) -> i64 {
widgets::ad_banner::create(unit_id_ptr as *const u8, size_ptr as *const u8)
}

/// Create a Button.
#[no_mangle]
pub extern "C" fn perry_ui_button_create(label_ptr: i64, on_press: f64) -> i64 {
Expand Down
105 changes: 105 additions & 0 deletions crates/perry-ui-windows/src/widgets/ad_banner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! `AdBanner` widget (#867) — Win32 layout placeholder.
//!
//! Google Mobile Ads ships no first-party Windows SDK, so — exactly like
//! the macOS backend (`perry-ui-macos/src/widgets/adbanner.rs`) — the
//! banner is a **layout placeholder**: an empty STATIC child sized to the
//! requested banner slot, so a `perry/ui` layout developed or previewed
//! on Windows reserves exactly the space the real ad occupies on
//! iOS/Android. The `unitId` is accepted for cross-platform API parity
//! but unused since nothing loads.
//!
//! Before this existed, `perry_ui_adbanner_create` was simply absent from
//! the Windows staticlib: any TS program using `AdBanner(...)` failed at
//! link time with an unresolved external (the symbol is a required
//! dispatch row in `perry-dispatch/src/ui_table/part_a.rs`).

#[cfg(target_os = "windows")]
use windows::Win32::Foundation::*;
#[cfg(target_os = "windows")]
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
#[cfg(target_os = "windows")]
use windows::Win32::UI::WindowsAndMessaging::*;

use super::{alloc_control_id, register_widget, WidgetKind};

fn str_from_header(ptr: *const u8) -> &'static str {
if ptr.is_null() {
return "";
}
unsafe {
let header = ptr as *const perry_runtime::string::StringHeader;
let len = (*header).byte_len as usize;
let data = ptr.add(std::mem::size_of::<perry_runtime::string::StringHeader>());
std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len))
}
}
Comment on lines +25 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Prevent Undefined Behavior by safely parsing potentially invalid UTF-8 strings.

The perry_runtime::string::StringHeader explicitly documents that strings may contain lone surrogates (WTF-8). Calling std::str::from_utf8_unchecked on such strings invokes Undefined Behavior in Rust. Use std::str::from_utf8 and safely fall back (e.g., to an empty string) if the bytes are invalid.

Additionally, returning &'static str for a runtime-allocated string is semantically incorrect because the string's lifetime is bound to the runtime's memory, not the life of the program. It should use a bounded lifetime parameter.

🔒️ Proposed fix
-fn str_from_header(ptr: *const u8) -> &'static str {
+fn str_from_header<'a>(ptr: *const u8) -> &'a str {
     if ptr.is_null() {
         return "";
     }
     unsafe {
         let header = ptr as *const perry_runtime::string::StringHeader;
         let len = (*header).byte_len as usize;
         let data = ptr.add(std::mem::size_of::<perry_runtime::string::StringHeader>());
-        std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len))
+        std::str::from_utf8(std::slice::from_raw_parts(data, len)).unwrap_or("")
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-ui-windows/src/widgets/ad_banner.rs` around lines 25 - 35,
Update str_from_header to use a bounded lifetime tied to the input pointer
instead of returning &'static str, and replace from_utf8_unchecked with
std::str::from_utf8. Return the existing empty-string fallback when the byte
slice is invalid while preserving the null-pointer behavior.


#[cfg(target_os = "windows")]
fn to_wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}

/// Banner dimensions in logical pixels for each size key the d.ts exposes.
/// Values match Google Mobile Ads' standard `AdSize` constants (and the
/// macOS placeholder) so the reserved slot lines up with the real
/// iOS/Android banner.
fn banner_size(size_key: &str) -> (f64, f64) {
match size_key {
"large-banner" => (320.0, 100.0),
"medium-rectangle" => (300.0, 250.0),
"full-banner" => (468.0, 60.0),
"leaderboard" => (728.0, 90.0),
// "banner" / "adaptive" / empty / unknown → standard 320×50.
_ => (320.0, 50.0),
}
}

/// Create the banner placeholder sized per `size_ptr`. Returns the widget
/// handle. The slot dimensions are pinned via fixed width/height so the
/// stack layout reserves them exactly (matching the macOS NSView frame).
pub fn create(unit_id_ptr: *const u8, size_ptr: *const u8) -> i64 {
let _unit_id = str_from_header(unit_id_ptr);
let size_key = str_from_header(size_ptr);
let (w, h) = banner_size(size_key);
let scale = crate::app::get_dpi_scale();
let w = (w * scale).round() as i32;
let h = (h * scale).round() as i32;
let control_id = alloc_control_id();

#[cfg(target_os = "windows")]
{
let class_name = to_wide("STATIC");
let window_text = to_wide("");
unsafe {
let hinstance = GetModuleHandleW(None).unwrap();
let hwnd = CreateWindowExW(
WINDOW_EX_STYLE::default(),
windows::core::PCWSTR(class_name.as_ptr()),
windows::core::PCWSTR(window_text.as_ptr()),
WINDOW_STYLE(WS_CHILD.0 | WS_VISIBLE.0),
0,
0,
w,
h,
Some(super::get_parking_hwnd()),
Some(HMENU(control_id as *mut _)),
Some(HINSTANCE::from(hinstance)),
None,
)
.unwrap();

let handle = register_widget(hwnd, WidgetKind::Image, control_id);
super::set_fixed_width(handle, w);
super::set_fixed_height(handle, h);
handle
}
}

#[cfg(not(target_os = "windows"))]
{
let handle = register_widget(0, WidgetKind::Image, control_id);
super::set_fixed_width(handle, w);
super::set_fixed_height(handle, h);
handle
}
}
1 change: 1 addition & 0 deletions crates/perry-ui-windows/src/widgets/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Widget registry — Vec<WidgetEntry> with 1-based handles.
//! Each widget has an HWND (on Windows), a kind, children list, and layout info.

pub mod ad_banner;
pub mod attributed_text;
pub mod bloomview;
pub mod bottom_nav;
Expand Down
Loading