/* * sharedspice client stub — the editor side of the ngspice_service split * (docs/features/ngspice-split/; the SPICE analog of exporter_step_stub.cpp). * * eeschema's NGSPICE class normally dlopens libngspice and binds ~10 function * pointers; in WASM the simulator engine lives in a separate worker module * (wasm/ngspice-service/), and NGSPICE::init_dll()'s __EMSCRIPTEN__ branch * binds its pointers to the pcbjam_ngSpice_* functions here instead. Each * forwards over the `globalThis.ngspiceService` provider * (web/standalone/src/wasm/ngspice-service.ts) via EM_ASYNC_JS — the editor * suspends through Asyncify while the worker answers (the `__asyncjs__*` * import is auto-covered by scripts/common/asyncify-imports.txt). * * Callbacks: KiCad registers its cbSendChar/cbSendStat/cbControlledExit/ * cbBGThreadRunning with pcbjam_ngSpice_Init; the worker streams `{ evt }` * frames which the provider hands to `globalThis.__ngspiceOnEvent` (installed * here). The dispatcher calls the exported pcbjam_ngspice_event — a fresh * WASM entry from JS, safe while the main C++ stack is Asyncify-suspended * (the wx-dom DOM-event mechanism, wxwidgets/src/wasm/domevents.cpp); KiCad's * callbacks only take a mutex and wxQueueEvent, so nothing on this path can * suspend. * * ngSpice_running stays cheap: a client-side atomic mirror maintained from * command results and bg events — the simulator UI polls it on a refresh * timer and an RPC per poll would be pure overhead. * * Netlist file shipping: NETLIST_EXPORTER_SPICE emits `.include ""` * lines (Sim.Library models, the IBIS cache) that ngspice opens from ITS * filesystem — pcbjam_ngSpice_Circ scans the deck, reads those files from the * editor MEMFS (recursively, bounded), and ships them with the circ request * so the service stages them at identical paths. */ #ifdef __EMSCRIPTEN__ #include #include #include #include #include #include #include #include #include #include #include using nlohmann::json; // ------------------------------------------------------------------------- // JS bridges // ------------------------------------------------------------------------- // Generic request: JSON in, JSON out (malloc'd; caller frees). Vector data // never travels this path — see js_ngspice_get_vec. // clang-format off EM_ASYNC_JS( char*, js_ngspice_request, ( const char* aReqJson ), { let res; try { const svc = globalThis.ngspiceService; if( !svc ) res = { error: 'ngspiceService provider not installed' }; else res = await svc.request( JSON.parse( UTF8ToString( aReqJson ) ) ); } catch( e ) { res = { error: String( e ) }; } const s = JSON.stringify( res ?? {} ); const n = lengthBytesUTF8( s ) + 1; const p = _malloc( n ); stringToUTF8( s, p, n ); return p; } ); // Vector fetch: fills editor-heap buffers directly (no JSON for MB arrays). // aMeta: int[4] = { found, vtype, flags, length }; aReal/aComp receive // malloc'd double buffers (comp interleaved re,im — the ngcomplex_t layout); // aVName receives a malloc'd name string. Returns non-zero on transport error. EM_ASYNC_JS( int, js_ngspice_get_vec, ( const char* aName, int* aMeta, double** aReal, double** aComp, char** aVName ), { let res; try { const svc = globalThis.ngspiceService; res = svc ? await svc.request( { kind: 'get_vec_info', name: UTF8ToString( aName ) } ) : { error: 'ngspiceService provider not installed' }; } catch( e ) { res = { error: String( e ) }; } HEAP32[aMeta >> 2] = 0; HEAPU32[aReal >> 2] = 0; HEAPU32[aComp >> 2] = 0; HEAPU32[aVName >> 2] = 0; if( !res || res.error ) return 1; if( !res.found ) return 0; HEAP32[( aMeta >> 2 ) + 1] = res.vtype | 0; HEAP32[( aMeta >> 2 ) + 2] = res.flags | 0; HEAP32[( aMeta >> 2 ) + 3] = res.length | 0; if( res.real && res.real.length ) { const p = _malloc( res.real.length * 8 ); HEAPF64.set( res.real, p >> 3 ); HEAPU32[aReal >> 2] = p; } if( res.comp && res.comp.length ) { const p = _malloc( res.comp.length * 8 ); HEAPF64.set( res.comp, p >> 3 ); HEAPU32[aComp >> 2] = p; } const s = res.vname || ''; const n = lengthBytesUTF8( s ) + 1; const vp = _malloc( n ); stringToUTF8( s, vp, n ); HEAPU32[aVName >> 2] = vp; HEAP32[aMeta >> 2] = 1; return 0; } ); // Event dispatcher: provider `{ evt }` frames -> KiCad's registered callbacks // via the exported pcbjam_ngspice_event (fresh wasm entries; see header // comment). Installed once, at first pcbjam_ngSpice_Init. EM_JS( void, js_ngspice_install_events, (), { if( globalThis.__ngspiceOnEvent ) return; globalThis.__ngspiceOnEvent = ( evt ) => { const call = ( kind, text, a, b ) => { let p = 0; if( text != null ) { const n = lengthBytesUTF8( text ) + 1; p = _malloc( n ); stringToUTF8( text, p, n ); } Module._pcbjam_ngspice_event( kind, p, a | 0, b | 0 ); }; if( evt.kind === 'char' || evt.kind === 'stat' ) { for( const line of evt.lines || [] ) call( evt.kind === 'char' ? 0 : 1, line, 0, 0 ); } else if( evt.kind === 'bg' ) { call( 2, null, evt.finished ? 1 : 0, 0 ); } else if( evt.kind === 'exit' ) { call( 3, null, evt.status | 0, ( evt.immediate ? 1 : 0 ) | ( evt.quit ? 2 : 0 ) ); } }; } ); // clang-format on // ------------------------------------------------------------------------- // Client state // ------------------------------------------------------------------------- namespace { SendChar* s_sendChar = nullptr; SendStat* s_sendStat = nullptr; ControlledExit* s_controlledExit = nullptr; BGThreadRunning* s_bgThreadRunning = nullptr; void* s_user = nullptr; // Mirror of the service's bg-run state (see header comment). std::atomic s_bgRunning{ false }; json rpc( const json& aReq ) { char* raw = js_ngspice_request( aReq.dump().c_str() ); json res = json::parse( raw ? raw : "{}", nullptr, /* allow_exceptions */ false ); std::free( raw ); if( res.is_discarded() ) res = json::object(); if( res.contains( "error" ) ) { fprintf( stderr, "[sharedspice_client] %s: %s\n", aReq.value( "kind", "?" ).c_str(), res["error"].dump().c_str() ); } return res; } // Read an editor-MEMFS file; returns false if it doesn't exist. bool readFile( const std::string& aPath, std::string* aOut ) { FILE* fp = fopen( aPath.c_str(), "rb" ); if( !fp ) return false; fseek( fp, 0, SEEK_END ); long size = ftell( fp ); fseek( fp, 0, SEEK_SET ); aOut->resize( size > 0 ? (size_t) size : 0 ); if( size > 0 && fread( aOut->data(), 1, (size_t) size, fp ) != (size_t) size ) { fclose( fp ); return false; } fclose( fp ); return true; } // Extract the file path from a `.include ""` / `.inc` / `.lib "" // [section]` deck line; empty if the line is not an include directive. std::string includePathFromLine( const std::string& aLine ) { size_t i = 0; while( i < aLine.size() && isspace( (unsigned char) aLine[i] ) ) i++; if( i >= aLine.size() || aLine[i] != '.' ) return std::string(); size_t wordEnd = i; while( wordEnd < aLine.size() && !isspace( (unsigned char) aLine[wordEnd] ) ) wordEnd++; std::string word = aLine.substr( i, wordEnd - i ); for( char& c : word ) c = (char) tolower( (unsigned char) c ); if( word != ".include" && word != ".inc" && word != ".lib" ) return std::string(); size_t p = wordEnd; while( p < aLine.size() && isspace( (unsigned char) aLine[p] ) ) p++; if( p >= aLine.size() ) return std::string(); if( aLine[p] == '"' || aLine[p] == '\'' ) { char quote = aLine[p++]; size_t end = aLine.find( quote, p ); return end == std::string::npos ? std::string() : aLine.substr( p, end - p ); } size_t end = p; while( end < aLine.size() && !isspace( (unsigned char) aLine[end] ) ) end++; return aLine.substr( p, end - p ); } // Collect the deck's referenced model files (recursively — a shipped library // may itself .include others), bounded against cycles and runaway depth. void collectIncludeFiles( const std::vector& aLines, json* aFiles, std::set* aSeen, int aDepth ) { if( aDepth > 4 ) return; for( const std::string& line : aLines ) { std::string path = includePathFromLine( line ); if( path.empty() || aSeen->count( path ) ) continue; aSeen->insert( path ); std::string text; if( !readFile( path, &text ) ) continue; // ngspice will report the miss with its native error aFiles->push_back( { { "path", path }, { "text", text } } ); std::vector nested; size_t start = 0; while( start <= text.size() ) { size_t nl = text.find( '\n', start ); if( nl == std::string::npos ) { nested.push_back( text.substr( start ) ); break; } nested.push_back( text.substr( start, nl - start ) ); start = nl + 1; } collectIncludeFiles( nested, aFiles, aSeen, aDepth + 1 ); } } } // namespace // ------------------------------------------------------------------------- // Event entry from JS (fresh wasm entry; must never suspend) // ------------------------------------------------------------------------- extern "C" EMSCRIPTEN_KEEPALIVE void pcbjam_ngspice_event( int aKind, char* aText, int aA, int aB ) { switch( aKind ) { case 0: // char if( s_sendChar ) s_sendChar( aText ? aText : const_cast( "" ), 0, s_user ); break; case 1: // stat if( s_sendStat ) s_sendStat( aText ? aText : const_cast( "" ), 0, s_user ); break; case 2: // bg: aA = finished s_bgRunning.store( aA == 0 ); if( s_bgThreadRunning ) s_bgThreadRunning( aA != 0, 0, s_user ); break; case 3: // exit: aA = status, aB = immediate|quit<<1 s_bgRunning.store( false ); if( s_controlledExit ) s_controlledExit( aA, ( aB & 1 ) != 0, ( aB & 2 ) != 0, 0, s_user ); break; } std::free( aText ); } // ------------------------------------------------------------------------- // The sharedspice API surface NGSPICE::init_dll binds to // ------------------------------------------------------------------------- void pcbjam_ngSpice_Init( SendChar* aSendChar, SendStat* aSendStat, ControlledExit* aExit, SendData*, SendInitData*, BGThreadRunning* aBgRunning, void* aUser ) { s_sendChar = aSendChar; s_sendStat = aSendStat; s_controlledExit = aExit; s_bgThreadRunning = aBgRunning; s_user = aUser; js_ngspice_install_events(); // Boots the worker lazily; a failure surfaces on the first command too // (KiCad ignores ngSpice_Init's status, matching its native call). rpc( { { "kind", "init" } } ); } int pcbjam_ngSpice_Circ( char** aCircArray ) { std::vector lines; for( int i = 0; aCircArray && aCircArray[i]; i++ ) lines.emplace_back( aCircArray[i] ); json files = json::array(); std::set seen; collectIncludeFiles( lines, &files, &seen, 0 ); json req = { { "kind", "circ" }, { "lines", lines }, { "files", std::move( files ) } }; return rpc( req ).value( "ret", 1 ); } int pcbjam_ngSpice_Command( char* aCommand ) { std::string cmd = aCommand ? aCommand : ""; int ret = rpc( { { "kind", "command" }, { "cmd", cmd } } ).value( "ret", 1 ); // The bg 'started' event arrives asynchronously; flip the mirror at the // acceptance edge so an immediate IsRunning() poll already sees it. if( ret == 0 && cmd.rfind( "bg_run", 0 ) == 0 ) s_bgRunning.store( true ); return ret; } pvector_info pcbjam_ngGet_Vec_Info( char* aVecName ) { // Per-call arena: valid until the next call, matching every NGSPICE // consumer (they copy within the same call). static vector_info s_vi; static char* s_name = nullptr; static double* s_real = nullptr; static double* s_comp = nullptr; std::free( s_name ); std::free( s_real ); std::free( s_comp ); s_name = nullptr; s_real = nullptr; s_comp = nullptr; int meta[4] = { 0, 0, 0, 0 }; double* real = nullptr; double* comp = nullptr; char* vname = nullptr; if( js_ngspice_get_vec( aVecName ? aVecName : "", meta, &real, &comp, &vname ) != 0 ) return nullptr; if( !meta[0] ) return nullptr; s_name = vname; s_real = real; s_comp = comp; s_vi.v_name = s_name; s_vi.v_type = meta[1]; s_vi.v_flags = (short) meta[2]; s_vi.v_length = meta[3]; s_vi.v_realdata = s_real; // Interleaved re,im doubles ARE the ngcomplex_t array layout. s_vi.v_compdata = reinterpret_cast( s_comp ); return &s_vi; } char* pcbjam_ngSpice_CurPlot( void ) { static std::string s_plot; s_plot = rpc( { { "kind", "cur_plot" } } ).value( "name", "" ); return s_plot.data(); } namespace { // Shared marshalling for the two NULL-terminated string-array calls. char** stringArrayResult( const json& aRes ) { static std::vector s_store; static std::vector s_ptrs; s_store.clear(); s_ptrs.clear(); if( aRes.contains( "names" ) && aRes["names"].is_array() ) { for( const auto& n : aRes["names"] ) s_store.push_back( n.get() ); } for( std::string& s : s_store ) s_ptrs.push_back( s.data() ); s_ptrs.push_back( nullptr ); return s_ptrs.data(); } } // namespace char** pcbjam_ngSpice_AllPlots( void ) { return stringArrayResult( rpc( { { "kind", "all_plots" } } ) ); } char** pcbjam_ngSpice_AllVecs( char* aPlotName ) { return stringArrayResult( rpc( { { "kind", "all_vecs" }, { "plot", aPlotName ? aPlotName : "" } } ) ); } bool pcbjam_ngSpice_Running( void ) { return s_bgRunning.load(); } char* pcbjam_ngCM_Input_Path( const char* aPath ) { static std::string s_path; s_path = aPath ? aPath : ""; rpc( { { "kind", "cm_input_path" }, { "path", s_path } } ); return s_path.data(); } #endif // __EMSCRIPTEN__