pcbjam/tests/apps/standalone/async-preload/async_preload_test.cpp
Viktor Vaczi c1ef489cfa feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on)
Replace the legacy Emscripten JS-exceptions model with native wasm-EH (legacy
encoding) across the whole build, keeping Asyncify coroutines working via a
from-source Binaryen --hoist-cpp-catches pre-pass. Net result: native-EH is the
only build mode, the 3D viewer is on by default, and pcbnew shrinks substantially.

Highlights:
- Binaryen submodule everywhere + --hoist-cpp-catches integration in apply-asyncify;
  post-link Asyncify covers every app wasm (not just standalone test wasm).
- Build deps (incl. OpenCASCADE without OCC_CONVERT_SIGNALS) and all KiCad apps
  with -fwasm-exceptions; emscripten_sleep added to the post-link asyncify-imports.
- libcontext fiber entry wired under native exceptions; while-loop main loop +
  currData shim injected into all wx apps.
- Native-EH collab apply fixed: DEBUG-define the embind TU + match all out-of-CMake
  C++ TUs' ABI flags to the core, fixing the vtable-layout skew / mis-dispatch.
- 3D viewer enabled by default (real raytracer linked, not the stub).
- Retire the EH-spike scaffolding; flip the asyncify-races ablation pins to
  shim-redundancy pins (native-EH stays clean with the legacy shims ablated).
- Fix the asyncify-races quiescence check to not require Asyncify.currData==0:
  under the native-EH per-frame-yield top loop the main stack is asyncify-suspended
  every frame, so currData legitimately churns (a freed-but-not-yet-nulled buffer,
  not a leak). Refresh the pcbnew toolbar screenshot baseline for the new kicad.
- CI: drop the obsolete binaryen_version input/env (the build uses the binaryen
  submodule fork's wasm-opt, not a version download); key the wasm-output cache on
  the binaryen submodule SHA instead.

Bumps the wxwidgets + binaryen submodules to their squashed feature commits.

Validated green: all 7 apps native-EH (real 3D in pcbnew); KiCad e2e 63/63
Firefox + Chromium (3D viewer renders); wx 336; coroutine 34/34 both engines;
asyncify 7/7 both engines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 09:40:26 +02:00

296 lines
10 KiB
C++

/**
* async_preload_test.cpp
*
* Standalone repro of KiCad-10's library-preload SHAPE (docs/features/wasm-exceptions/10 §7) —
* NOT using real KiCad/pcbnew. Proves native wasm-EH makes the shape safe.
*
* The shape (mirrors kicad/eeschema/sch_io/pcbjam_lib/sch_io_pcbjam_lib.cpp):
* - a std::async(std::launch::async, …) background worker (NOT the thread pool) that, per
* "library", PROXIES a fetch to the main thread (emscripten_proxy_sync_with_ctx) and then
* PARSES the bytes on the worker — the parse THROWS an IO_ERROR-like exception (the "mode-c"
* trigger: a throw caught ON a worker drives Asyncify under -fexceptions → crash; native
* wasm-EH decouples it → safe).
* - a LAZY join: main keeps the std::future alive and never blocks in normal operation, so it
* stays in its event loop to service the worker's proxied fetches.
* - a g_proxyMutex serializes worker→main proxy round-trips (the real "table index out of
* bounds" reentrancy guard when main is asyncify-suspended in a modal).
*
* (Simplification vs real KiCad: the proxied fetch finishes synchronously in the handler — the real
* one kicks an async JS promise + finishes from its callback. We keep the round-trip + the
* serialization, which is what the reentrancy hazard and the mode-c throw need; the throw is on the
* worker's parse, independent of the fetch being sync/async.)
*
* URL ?m=0 simple | 1 throw (parse throws → caught on worker, no mode-c crash) |
* 2 shutdown (main blocking-joins the future mid-load) | 3 modal (a modal opens while
* workers proxy → g_proxyMutex must prevent a reentrancy crash)
*
* Console contract:
* [PRELOAD] START m=.. [PRELOAD] EH=native|js
* [PRELOAD] SUCCESS m=.. caught=.. loaded=..
*/
#include "wx/wx.h"
#include <atomic>
#include <chrono>
#include <cstdarg>
#include <cstdio>
#include <future>
#include <mutex>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#include <emscripten/proxying.h>
#include <emscripten/threading.h>
#endif
// ---------------------------------------------------------------------------
static void plog( const char* fmt, ... )
{
char buf[512];
va_list ap;
va_start( ap, fmt );
vsnprintf( buf, sizeof( buf ), fmt, ap );
va_end( ap );
#ifdef __EMSCRIPTEN__
EM_ASM( { console.log( UTF8ToString( $0 ) ); }, buf );
#else
printf( "%s\n", buf );
#endif
}
static int readMode()
{
#ifdef __EMSCRIPTEN__
return EM_ASM_INT( {
var raw = location.search ? location.search.slice( 1 ) : location.hash.slice( 1 );
var v = parseInt( new URLSearchParams( raw ).get( 'm' ), 10 );
return isNaN( v ) ? 0 : v;
} );
#else
return 0;
#endif
}
static void mainSleep( int ms )
{
#ifdef __EMSCRIPTEN__
emscripten_sleep( ms ); // Asyncify yield → main's event loop runs (services the proxy queue)
#else
std::this_thread::sleep_for( std::chrono::milliseconds( ms ) );
#endif
}
// ---------------------------------------------------------------------------
// the proxied "fetch": worker → main round-trip (mirrors the PCBJAM bridge)
// ---------------------------------------------------------------------------
static std::mutex g_proxyMutex;
#ifdef __EMSCRIPTEN__
extern "C" EMSCRIPTEN_KEEPALIVE void preload_finish( em_proxying_ctx* ctx )
{
emscripten_proxy_finish( ctx );
}
static void proxy_fetch_on_main( em_proxying_ctx* ctx, void* )
{
// Runs on the MAIN thread via the system proxying queue. Synchronous finish (see header note).
preload_finish( ctx );
}
#endif
static std::string proxyFetchToMain( const std::string& lib )
{
#ifdef __EMSCRIPTEN__
if( emscripten_is_main_runtime_thread() )
return "(mock " + lib + ")"; // main path (not exercised here; the preload runs on a worker)
std::lock_guard<std::mutex> serialize( g_proxyMutex );
em_proxying_queue* q = emscripten_proxy_get_system_queue();
if( !emscripten_proxy_sync_with_ctx( q, emscripten_main_runtime_thread_id(),
proxy_fetch_on_main, nullptr ) )
return "";
#endif
return "(mock " + lib + ")";
}
// ---------------------------------------------------------------------------
class IO_ERROR : public std::runtime_error
{
public:
explicit IO_ERROR( const std::string& m ) : std::runtime_error( m ) {}
};
static std::size_t parseLib( const std::string& data, bool shouldThrow )
{
if( shouldThrow )
throw IO_ERROR( "simulated S-expr parse failure" ); // the mode-c trigger
return data.find( "mock" ) != std::string::npos ? 1u : 0u;
}
// ---------------------------------------------------------------------------
static std::atomic<bool> g_caught{ false };
static std::atomic<int> g_loaded{ 0 };
static std::atomic<bool> g_done{ false };
static std::atomic<bool> g_abort{ false };
static std::future<void> g_future; // kept alive => LAZY join (dtor blocks only at teardown)
static void preloadRun( bool throwOnParse )
{
try
{
std::this_thread::sleep_for( std::chrono::milliseconds( 20 ) ); // worker-side watchdog
for( const char* lib : { "lib_a", "lib_b", "lib_c" } )
{
if( g_abort.load() )
break; // the CancelPreload mitigation: bail before the next proxy
std::string data = proxyFetchToMain( lib ); // proxy the fetch to main
g_loaded.fetch_add( (int) parseLib( data, throwOnParse ) ); // parse ON the worker (throws)
}
}
catch( const std::exception& e )
{
g_caught.store( true );
plog( "[PRELOAD] worker caught: %s", e.what() );
}
g_done.store( true );
}
// ---------------------------------------------------------------------------
// minimal auto-closing modal (timer armed before ShowModal; closes itself)
// ---------------------------------------------------------------------------
static constexpr int ID_MODAL_CLOSE = wxID_HIGHEST + 700;
class AutoCloseDialog : public wxDialog
{
public:
AutoCloseDialog( wxWindow* parent, int delayMs ) :
wxDialog( parent, wxID_ANY, "preload-modal", wxDefaultPosition, wxSize( 280, 120 ) ),
m_timer( this, ID_MODAL_CLOSE )
{
Bind( wxEVT_SHOW, &AutoCloseDialog::OnShow, this );
Bind( wxEVT_TIMER, [this] ( wxTimerEvent& ) { plog( "[PRELOAD] modal: close-timer fired" ); EndModal( wxID_OK ); }, ID_MODAL_CLOSE );
m_delayMs = delayMs;
}
private:
void OnShow( wxShowEvent& e )
{
if( e.IsShown() )
{
plog( "[PRELOAD] modal: shown, arming %dms auto-close", m_delayMs );
m_timer.StartOnce( m_delayMs );
}
e.Skip();
}
wxTimer m_timer;
int m_delayMs = 300;
};
// ---------------------------------------------------------------------------
static constexpr int ID_SCENARIO_TIMER = wxID_HIGHEST + 701;
class PreloadFrame : public wxFrame
{
public:
PreloadFrame() : wxFrame( nullptr, wxID_ANY, "Async Preload Test",
wxDefaultPosition, wxSize( 420, 140 ) ),
m_scenarioTimer( this, ID_SCENARIO_TIMER )
{
wxPanel* p = new wxPanel( this );
wxBoxSizer* s = new wxBoxSizer( wxVERTICAL );
s->Add( new wxStaticText( p, wxID_ANY, "std::async library-preload repro — see console." ),
0, wxALL, 16 );
p->SetSizer( s );
Bind( wxEVT_TIMER, &PreloadFrame::OnScenario, this, ID_SCENARIO_TIMER );
}
void armModalScenario() { m_scenarioTimer.StartOnce( 50 ); }
private:
// Runs INSIDE the main event loop (the modal pump needs that — ShowModal straight from OnInit,
// before the loop starts, hangs). Several workers each do a bounded series of worker->main proxy
// round-trips with a small gap so the modal pump can dispatch its auto-close timer; g_proxyMutex
// serializes them, preventing concurrent C reentry into the asyncify-suspended (modal) main.
void OnScenario( wxTimerEvent& )
{
std::atomic<int> rounds{ 0 };
std::vector<std::thread> workers;
for( int i = 0; i < 3; ++i )
workers.emplace_back( [&rounds]
{
for( int k = 0; k < 12; ++k )
{
proxyFetchToMain( "spam" );
rounds.fetch_add( 1 );
std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) );
}
} );
plog( "[PRELOAD] modal: workers spawned, showing modal" );
AutoCloseDialog dlg( this, 300 );
dlg.ShowModal();
plog( "[PRELOAD] modal: ShowModal returned (rounds=%d)", rounds.load() );
for( auto& t : workers )
t.join();
plog( "[PRELOAD] SUCCESS m=3 caught=0 loaded=0 proxyRounds=%d", rounds.load() );
}
wxTimer m_scenarioTimer;
};
class PreloadApp : public wxApp
{
public:
bool OnInit() override
{
const int mode = readMode();
plog( "[PRELOAD] START m=%d", mode );
#ifdef __WASM_EXCEPTIONS__
plog( "[PRELOAD] EH=native" );
#else
plog( "[PRELOAD] EH=js" );
#endif
g_caught.store( false );
g_loaded.store( 0 );
g_done.store( false );
g_abort.store( false );
PreloadFrame* frame = new PreloadFrame();
frame->Show();
if( mode == 3 )
{
frame->armModalScenario(); // runs in the main loop; logs its own [PRELOAD] SUCCESS m=3
return true;
}
const bool throwOnParse = ( mode == 1 );
g_future = std::async( std::launch::async, [throwOnParse] { preloadRun( throwOnParse ); } );
if( mode == 2 ) // shutdown: blocking-join the future mid-load (synchronous proxies complete
// during the futex busy-wait's queue processing)
{
mainSleep( 30 );
g_abort.store( true );
g_future.wait();
plog( "[PRELOAD] shutdown joined" );
}
else // simple / throw: LAZY join — poll via emscripten_sleep, main stays live
{
for( int i = 0; i < 200 && !g_done.load(); ++i )
mainSleep( 20 );
}
plog( "[PRELOAD] SUCCESS m=%d caught=%d loaded=%d",
mode, g_caught.load() ? 1 : 0, g_loaded.load() );
return true;
}
};
wxIMPLEMENT_APP( PreloadApp );