PrimJS Documentation

repository·develop·Indexed 22 days ago

https://github.com/lynx-family/primjs

PrimJS 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.

Tokens
26.7K
Snippets
78
Records
112
Agent score
78%

What's inside PrimJS

  1. What is PrimJS?

    develop
    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.
  2. Overview of PrimJS Key Features

    develop

    PrimJS 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.
  3. Performance comparison between PrimJS and QuickJS

    develop

    PrimJS 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**             |
  4. How breakpoints work in PrimJS

    develop

    PrimJS manages breakpoints using a linked list of LEPUSBreakpoint data structures within the execution context.

    • Setting Breakpoints: When a user sets a breakpoint, the front-end sends Debugger.setBreakpointByUrl. PrimJS populates a LEPUSBreakpoint object with the line number, column number, and script ID, then adds it to the breakpoint linked list.
    • Removing Breakpoints: When Debugger.removeBreakpoint is 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 RunMessageLoopOnPause to halt the thread and dispatches a Debugger.paused message containing the call stack and variables.

    Related Protocols:

    • Debugger.setBreakpoint
    • Debugger.setBreakpointByUrl
    • Debugger.setBreakpointsActive
    • Debugger.getPossibleBreakpoints
    • Debugger.removeBreakpoint
    • Debugger.continueToLocation
  5. How platform linking works

    develop

    The 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.h header. The registration code is shared and auto-registers when loaded. Crucially, the host app must include addon_use.h in exactly one .cc or .mm translation unit to ensure the auto-registration symbol is retained before calling requireNodeAddon.
  6. How PrimJS handles source code display

    develop

    PrimJS implements source code visibility by following the Chrome DevTools Protocol (CDP).

    1. Script Parsing: After compiling a script, PrimJS proactively dispatches the Debugger.scriptParsed event. This event includes the script's ID and URL. PrimJS instantiates a LEPUSScriptSource and adds it to the current execution context's script list.
    2. Retrieving Source: When a debugging front-end requests source code via the Debugger.getScriptSource method, 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)
  7. Manage temporary heap objects with HandleScope

    develop

    When writing C/C++ code for the engine, any heap object (such as LEPUSValue or LEPUSAtom) held by a temporary variable must be recorded in a HandleScope. 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.

    HandleScope uses 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 HandleScope management, as their lifecycle is managed by the caller.
    • Scope-based management: Only use HandleScope for 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
  8. How bytecode dispatch works in the Template interpreter

    develop

    The Template interpreter executes by jumping between assembly blocks. At initialization, a dispatch_table is created where dispatch_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]
    ...
  9. How variables are displayed during debugging

    develop

    PrimJS enables variable inspection in the debugging panel (global, closure, or local scopes) by utilizing objectId mapping and the Chrome DevTools Protocol.

    1. Pausing and Scopes: When execution pauses (due to breakpoints, exceptions, or stepping), PrimJS dispatches a Debugger.paused message. This message includes a scopeChain field. Each scope (global, closure, or local) within a stack frame is assigned a unique objectId.
    2. Variable Inspection: Every variable within a scope is also assigned a unique objectId. When the front-end wants to inspect a variable, it sends a Runtime.getProperties message containing that objectId. PrimJS uses this ID to identify the specific variable or scope and returns its properties.

    Related Protocols:

    • Debugger.evaluateOnCallFrame
    • Runtime.compileScript
    • Runtime.getProperties
  10. How symbol renaming and weak macros work in @lynx-js/weak-node-api

    develop

    To prevent linkage and symbol conflicts, @lynx-js/weak-node-api uses 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_NAPI compile-time macro is defined during your build process. During the prepare:headers pipeline, these macro headers are copied into the headers/ directory so that consumers can include them directly from there.

  11. How the PrimJS Debugger works with Chrome DevTools Protocol

    develop

    The PrimJS debugging process follows the Chrome DevTools Protocol (CDP). The debugger operates in two primary states: Executing and Paused.

    Execution Flow

    1. 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.
    2. Pausing: When PrimJS encounters a breakpoint or receives a protocol message like Debugger.pause, it induces the current thread to pause by invoking runMessageLoopOnPause. This transitions the thread into a loop waiting state.
    3. Processing Messages: While paused, PrimJS uses dispatchProtocolMessage to process messages sent from the front-end (e.g., via LynxDevtool).
    4. Resuming: Upon receiving commands like Debugger.resume or Debugger.stepInto, PrimJS transitions back to the executing state and invokes quitMessageLoopOnPause to cancel the waiting loop.
  12. Implement symbol renaming with USE_WEAK_SUFFIX_NAPI

    develop

    To avoid symbol conflicts with other N-API providers, you can use the USE_WEAK_SUFFIX_NAPI macro. This is enabled by default on HarmonyOS and macOS.

    When implementing your addon logic in a .cc file, follow this specific include pattern for each translation unit:

    1. Place your standard includes.
    2. Include weak_napi_defines.h immediately after your last standard include.
    3. Implement your logic.
    4. Include weak_napi_undefs.h at 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"