PrimJS Documentation
repository·develop·Indexed 22 days ago
https://github.com/lynx-family/primjsPrimJS is a lightweight, high-performance JavaScript engine built on QuickJS and optimized for the Lynx cross-platform framework. It features an optimized template interpreter, GC-based memory management, ES2019 support, and Chrome DevTools Protocol integration. The repository also includes @lynx-js/weak-node-api, providing weak Node-API headers and a scaffolding CLI to create N-API addons for Android, iOS, HarmonyOS, and macOS using CMake.
What's inside PrimJS
- PrimJS is a lightweight, high-performance JavaScript engine built on top of QuickJS. It is designed specifically for the Lynx cross-platform framework and provides full support for ES2019. It aims to deliver superior performance and an improved development experience compared to standard QuickJS.
Overview of PrimJS Key Features
developPrimJS is a high-performance JavaScript engine built on top of QuickJS, specifically optimized for the Lynx cross-platform framework. Key technical advantages include:
- Optimized Interpreter: Uses stack caching and register optimizations via a template interpreter.
- Seamless Object Model Integration: Efficiently integrates with the Lynx object model to reduce data communication overhead.
- Advanced Memory Management: Uses a Garbage Collector (GC) instead of reference counting, improving performance and memory analyzability.
- Comprehensive Debugging: Implements the Chrome DevTools Protocol (CDP) for integration with Chrome Debugger.
- WebAssembly Support: Supports module loading, instantiation, and JS-to-WASM interoperability.
Performance comparison between PrimJS and QuickJS
developPrimJS performance is measured using the Octane Benchmark. In the tested environment (Apple M1 Max, 64GB RAM, macOS Sonoma), PrimJS generally outperforms QuickJS across most benchmarks, achieving a higher overall Score (version 9).
| BenchMark | QuickJS <br>(6e2e68) | PrimJS | |------------------|-----------------------|----------------------| | Richards | 1163 | 1247 | | DeltaBlue | 1093 | 1353 | | Crypto | 1349 | 1844 | | RayTrace | 1273 | 2751 | | NavierStokes | 2640 | 4166 | | Mandreel | 1350 | 1372 | | MandreelLatency | 9680 | 9587 | | Gameboy | 9265 | 10463 | | CodeLoad | 18137 | 16992 | | Box2D | 4544 | 5670 | | zlib | 3097 | 3864 | | Typescript | 18158 | 22855 | | EarleyBoyer | 2284 | 4270 | | RegExp | 282 | 324 | | PdfJS | 4236 | 6642 | | **Score (version 9)** | **2904** | **3735** |How breakpoints work in PrimJS
developPrimJS manages breakpoints using a linked list of
LEPUSBreakpointdata structures within the execution context.- Setting Breakpoints: When a user sets a breakpoint, the front-end sends
Debugger.setBreakpointByUrl. PrimJS populates aLEPUSBreakpointobject with the line number, column number, and script ID, then adds it to the breakpoint linked list. - Removing Breakpoints: When
Debugger.removeBreakpointis received, PrimJS locates the specific breakpoint in the linked list using its identifier and removes it. - Triggering Pauses: During bytecode execution, PrimJS checks the current position (line, column, and script ID) against the breakpoint list. If a match is found, PrimJS calls
RunMessageLoopOnPauseto halt the thread and dispatches aDebugger.pausedmessage containing the call stack and variables.
Related Protocols:
Debugger.setBreakpointDebugger.setBreakpointByUrlDebugger.setBreakpointsActiveDebugger.getPossibleBreakpointsDebugger.removeBreakpointDebugger.continueToLocation
- Setting Breakpoints: When a user sets a breakpoint, the front-end sends
How platform linking works
developThe project uses different linking strategies depending on the target platform:
- Android: The build downloads an AAR, extracts
vendor/android/libnapi_adapter.so, and links against it. - HarmonyOS: The build downloads a HAR, extracts
vendor/harmony/libnapi_adapter.so, and links against it. - iOS/macOS: These platforms emit static libraries and a generated
addon_use.hheader. The registration code is shared and auto-registers when loaded. Crucially, the host app must includeaddon_use.hin exactly one.ccor.mmtranslation unit to ensure the auto-registration symbol is retained before callingrequireNodeAddon.
- Android: The build downloads an AAR, extracts
How PrimJS handles source code display
developPrimJS implements source code visibility by following the Chrome DevTools Protocol (CDP).
- Script Parsing: After compiling a script, PrimJS proactively dispatches the
Debugger.scriptParsedevent. This event includes the script'sIDandURL. PrimJS instantiates aLEPUSScriptSourceand adds it to the current execution context's script list. - Retrieving Source: When a debugging front-end requests source code via the
Debugger.getScriptSourcemethod, PrimJS uses the provided script ID to locate the source in its internal script list and returns it to the front-end.
Related Protocols:
Debugger.scriptParsed(Event)Debugger.getScriptSource(Method)
- Script Parsing: After compiling a script, PrimJS proactively dispatches the
Manage temporary heap objects with HandleScope
developWhen writing C/C++ code for the engine, any heap object (such as
LEPUSValueorLEPUSAtom) held by a temporary variable must be recorded in aHandleScope. This ensures that if a Garbage Collection (GC) cycle is triggered (e.g., by an allocation call), the object is recognized as a 'root' and not erroneously collected.HandleScopeuses an expandable array to store object addresses and is typically used within a function scope. It automatically cleans up all recorded objects when the scope is destroyed.Usage Conventions:
- Parameters: Objects passed as function parameters do not need
HandleScopemanagement, as their lifecycle is managed by the caller. - Scope-based management: Only use
HandleScopefor objects whose lifecycle is tied to the current execution scope and might be subject to GC during that scope's execution.
// Example pattern for using HandleScope { HandleScope hs; LEPUSValue tmp = LEPUS_NewArray(ctx); // 'tmp' is now protected from GC because it is recorded in 'hs' // ... use tmp ... } // 'tmp' is no longer protected once 'hs' is destroyed- Parameters: Objects passed as function parameters do not need
How bytecode dispatch works in the Template interpreter
developThe Template interpreter executes by jumping between assembly blocks. At initialization, a
dispatch_tableis created wheredispatch_table[op]contains the starting address of the assembly instructions for that specific opcode (op).Each bytecode handler follows a pattern of executing its logic and then fetching the next opcode to jump to the next handler:
bytecode0_handler: ... // assembly ... ... opcode = *pc; // pc -> current bytecode pointer jump dispatch_table[opcode] bytecode1_handler: ... // assembly ... opcode = *pc; jump dispatch_table[opcode] ...How variables are displayed during debugging
developPrimJS enables variable inspection in the debugging panel (global, closure, or local scopes) by utilizing
objectIdmapping and the Chrome DevTools Protocol.- Pausing and Scopes: When execution pauses (due to breakpoints, exceptions, or stepping), PrimJS dispatches a
Debugger.pausedmessage. This message includes ascopeChainfield. Each scope (global, closure, or local) within a stack frame is assigned a uniqueobjectId. - Variable Inspection: Every variable within a scope is also assigned a unique
objectId. When the front-end wants to inspect a variable, it sends aRuntime.getPropertiesmessage containing thatobjectId. PrimJS uses this ID to identify the specific variable or scope and returns its properties.
Related Protocols:
Debugger.evaluateOnCallFrameRuntime.compileScriptRuntime.getProperties
- Pausing and Scopes: When execution pauses (due to breakpoints, exceptions, or stepping), PrimJS dispatches a
How symbol renaming and weak macros work in @lynx-js/weak-node-api
developTo prevent linkage and symbol conflicts,
@lynx-js/weak-node-apiuses a symbol renaming scheme via macro wrappers. This is implemented through two specific macro headers:defs_header/weak_napi_defines.h: Implements the weak symbol macro scheme.defs_header/weak_napi_undefs.h: Handles the undefining of macros.
Important Note on Usage: These weak symbol macro includes are only effective when the
USE_WEAK_SUFFIX_NAPIcompile-time macro is defined during your build process. During theprepare:headerspipeline, these macro headers are copied into theheaders/directory so that consumers can include them directly from there.How the PrimJS Debugger works with Chrome DevTools Protocol
developThe PrimJS debugging process follows the
Chrome DevTools Protocol(CDP). The debugger operates in two primary states: Executing and Paused.Execution Flow
- Executing State: Before executing each bytecode, PrimJS checks for pending protocol messages from the debugging front-end. If no messages are present, it proceeds with bytecode execution.
- Pausing: When PrimJS encounters a breakpoint or receives a protocol message like
Debugger.pause, it induces the current thread to pause by invokingrunMessageLoopOnPause. This transitions the thread into a loop waiting state. - Processing Messages: While paused, PrimJS uses
dispatchProtocolMessageto process messages sent from the front-end (e.g., viaLynxDevtool). - Resuming: Upon receiving commands like
Debugger.resumeorDebugger.stepInto, PrimJS transitions back to the executing state and invokesquitMessageLoopOnPauseto cancel the waiting loop.
Implement symbol renaming with USE_WEAK_SUFFIX_NAPI
developTo avoid symbol conflicts with other N-API providers, you can use the
USE_WEAK_SUFFIX_NAPImacro. This is enabled by default on HarmonyOS and macOS.When implementing your addon logic in a
.ccfile, follow this specific include pattern for each translation unit:- Place your standard includes.
- Include
weak_napi_defines.himmediately after your last standard include. - Implement your logic.
- Include
weak_napi_undefs.hat the very end of the file.
Example structure:
#include <some_header.h> #include "weak_napi_defines.h" // Your N-API logic here #include "weak_napi_undefs.h"