Codex stop-hook flagged: "web Step 3 cannot render the claimed
canvas text". Root cause: WebBackend::draw_text was a Phase A
no-op stub. CanvasViewport calls draw_text for "Hello
OpenPencil" / "Click me" / Layer panel labels / etc — none of
those text strings actually rendered in the browser.
# Fix
`crates/openpencil-shell-web/src/backend/mod.rs::WebBackend`:
- Embeds `assets/Roboto-Regular.ttf` (Apache 2.0, 35 KB, copied
from rust-skia test resources) via `include_bytes!`. The
C-hard wasm32-unknown-unknown skia build uses
`skia_enable_fontmgr_custom_empty=yes` (see
`vendor/skia-safe-op/skia-bindings/build_support/platform/
wasm_unknown.rs`), so there are no system fonts and we have
to bake the bytes in.
- New `typeface: Option<Typeface>` field, lazy-init on first
draw_text via `FontMgr::custom_empty().and_then(|m|
m.new_from_data(ROBOTO_TTF, None))`. Build failure → None
silently no-ops subsequent draws (no panic — text just
doesn't render).
- `draw_text` now iterates `layout.runs()` and calls
`Canvas::draw_str` per run with a `Font::new(typeface,
font_size)` + `Paint` from the run color.
`crates/openpencil-shell-web/Cargo.toml`:
- Drops `textlayout` skia-safe feature. We use raw `draw_str`
not paragraph builder; textlayout pulled ICU + Harfbuzz +
~400 KB gzip + a swarm of font-lookup imports we don't
exercise.
# 24 new env.* imports — wasm-libc-shim expansion
Even without textlayout, Skia's font path imports 24 libc
symbols our prior C-hard.2 shim didn't cover. All resolved:
`crates/wasm-libc-shim/src/imp.rs` — Rust extern "C" shims:
- string ops (real impls): strncmp, strncpy, strstr, strrchr,
strcat, strtol, tolower, qsort (insertion sort, fits Skia's
small-array call sites; debug_assert on element size > 256)
- file I/O (sentinel error returns, no filesystem on wasm):
fopen → null, fread → 0, fclose → 0, fputc → c, fileno → -1,
fstat → -1, pread → -1, ftell → -1, fseek → -1
- env: getenv → null
- mmap: returns MAP_FAILED ((void*)-1); munmap → -1
- setjmp/longjmp: setjmp returns 0 (treat as initial call);
longjmp panics — happy text path through in-memory TTF parse
should never trigger it
- C++ nothrow new: `_ZnwmRKSt9nothrow_t` forwards to malloc,
returns nullptr on OOM (the nothrow contract)
`crates/wasm-libc-shim/src/stdio_stub.c`:
- fprintf C-side variadic stub (Skia diagnostic path) that
routes into the same panic helper as snprintf / vsnprintf /
vfprintf — same fail-fast policy.
# Verification
- `cargo build -p openpencil-shell-web --target
wasm32-unknown-unknown --features skia --release` — green
- `wasm-bindgen --target web` produces ../pkg/openpencil_shell
_web.{js,_bg.wasm}
- `bash tools/check-wasm-bundle.sh` — PASS:
- 0 env.* imports preserved (24 new ones absorbed by shim)
- 906 935 bytes gzip = 86% of 1 MiB ceiling (+286 KB vs
pre-text — Skia font/freetype subsystem is substantial.
Headroom: 14% of ceiling)
- `cargo test -p openpencil-shell-core --lib` — 39 tests
- `cargo build -p openpencil-shell-native --example
inspector_window` — green (desktop demo unchanged — uses
jian-skia textlayout via NativeBackend, not the web font
path)
- `cargo check -p openpencil-shell-native --target
aarch64-apple-ios` — green
- `cargo check -p openpencil-shell-native --target
aarch64-linux-android` — green
# Re-run the demo
```
EMSDK="$HOME/.emsdk" cargo build -p openpencil-shell-web \
--target wasm32-unknown-unknown --features skia --release
wasm-bindgen --target web --out-dir crates/openpencil-shell-web/pkg \
target/wasm32-unknown-unknown/release/openpencil_shell_web.wasm
cd crates/openpencil-shell-web/smoke
python3 -m http.server 8000
# Browser: http://localhost:8000/step-1b.html
```
Now the canvas viewport renders "Hello OpenPencil" + "Click me"
text in addition to the rect/stroke geometry.
59 lines
2 KiB
C
59 lines
2 KiB
C
/*
|
|
* C-hard.2 stdio stubs for wasm32-unknown-unknown skia bundles.
|
|
*
|
|
* Skia uses snprintf/vsnprintf/vfprintf for SkString::printf style
|
|
* formatting (e.g. shader debug logs, font path debug). On the raster
|
|
* + custom_empty fontmgr pipeline none of these are exercised in
|
|
* normal rendering — verified by running the post-link bundle and
|
|
* confirming 0 env.* imports. If a path IS actually hit at runtime
|
|
* we want to FAIL FAST with the symbol name in the panic message
|
|
* rather than silently returning a successful empty string (codex
|
|
* Round 1 C4: silent empty masked unexpected printf reachability).
|
|
*
|
|
* Variadic in stable Rust requires the `c_variadic` nightly feature.
|
|
* Doing this in C keeps the wasm-libc-shim crate stable-compatible;
|
|
* the Rust side (`imp.rs::wasm_libc_shim_stdio_panic`) does the
|
|
* actual panic with `console_error_panic_hook` integration.
|
|
*/
|
|
|
|
#include <stddef.h>
|
|
#include <stdarg.h>
|
|
|
|
extern void wasm_libc_shim_stdio_panic(const char *name) __attribute__((noreturn));
|
|
|
|
int snprintf(char *buf, size_t size, const char *fmt, ...) {
|
|
(void)fmt;
|
|
if (buf && size > 0) {
|
|
buf[0] = 0;
|
|
}
|
|
wasm_libc_shim_stdio_panic("snprintf");
|
|
}
|
|
|
|
int vsnprintf(char *buf, size_t size, const char *fmt, va_list ap) {
|
|
(void)fmt;
|
|
(void)ap;
|
|
if (buf && size > 0) {
|
|
buf[0] = 0;
|
|
}
|
|
wasm_libc_shim_stdio_panic("vsnprintf");
|
|
}
|
|
|
|
int vfprintf(void *stream, const char *fmt, va_list ap) {
|
|
(void)stream;
|
|
(void)fmt;
|
|
(void)ap;
|
|
wasm_libc_shim_stdio_panic("vfprintf");
|
|
}
|
|
|
|
/* Step 3 codex stop-hook addition: fprintf is variadic so it
|
|
* needs the C-side stub treatment. Skia uses it for diagnostic
|
|
* messages on freetype error paths; happy text-rendering path
|
|
* doesn't reach it. Same fail-fast policy as the snprintf
|
|
* family — easier to find a real call than to debug an empty
|
|
* silent return. */
|
|
int fprintf(void *stream, const char *fmt, ...) {
|
|
(void)stream;
|
|
(void)fmt;
|
|
wasm_libc_shim_stdio_panic("fprintf");
|
|
}
|