Shaidin
Cross

Cross is a library for building applications for multiple platforms from a single codebase. Developers provide the application core in C++ and the user interface in HTML, JavaScript, and CSS.

                              ┌───────┐ ────► iOS (ipa)
                              │       │
                              │       │ ────► Android (aab)
┌───────────────────┐         │       │
│ C++ core + web UI │ ──────► │ Cross │ ────► Linux (deb)
└───────────────────┘         │       │
                              │       │ ────► Windows (msi)
                              │       │
                              └───────┘ ────► Web (html/css/js/wasm)

cross-core

cross-core defines the shared runtime and application-facing interfaces used by Cross applications.

Setup project

  1. Create manifest.txt at the project root. For example:

    target=Example
    identifier=com.example.app
    version=1.0.0
    internet=false
    guid=<UUID>

    internet controls Android network permission. guid is the stable Windows installer upgrade GUID.

  2. Create icon.svg at the project root.

  3. Put the HTML, JavaScript, CSS, and other UI resources in the root assets folder.

  4. Implement the application contract in the root src folder.

  5. Derive each logical application stage from core::Stage, with its source files in the root src folder. Implement the pure virtual Escape and FeedUri methods and any lifecycle hooks the stage needs.

  6. Return a non-null requested stage from application::CreateStage. Cross owns its lifetime.

Application interface

Application contract

The application implements the persistence and stage-factory callbacks declared by cross-core:

namespace application
{
    using Completion = std::function<void()>;

    void Restore(std::istream& input, Completion completion);
    void Checkpoint(std::ostream& output);
    std::unique_ptr<core::Stage> CreateStage();
}

UI and stages

Cross hosts one stage at a time. An application can replace that stage as it moves between logical parts. Business logic belongs in a core::Stage-derived class. A stage normally loads its UI from Attach:

bridge::LoadView(Index(), "document");

Platform adapters resolve that name to assets/document.htm and initialize the bridge in the loaded document.

Core-to-UI communication

Application code can inject a JavaScript expression into the current UI with a C-style string:

void bridge::CallFunction(const char* function);

UI-to-core communication

The UI sends a message to the current stage by calling CallHandler(id, command, info). All three arguments are strings; use an empty string for info when the message has no payload. For portability across the platform adapters, id and command must be non-empty and contain no whitespace.

Derived stages register message callbacks in the protected handlers_ map. A map key corresponds to the message id supplied by the UI:

using HANDLER = std::function<void(const char* command, const char* info)>;
std::map<std::string, HANDLER> handlers_;

Binary content

Images, audio, downloads, and other HTML resources can be supplied as binary data by overriding Stage::FeedUri:

virtual void FeedUri(const char* uri, std::function<void(
    const std::vector<unsigned char>&)>&& consume) = 0;

Cross validates cross://<receiver>/<path> requests for the active stage and passes only <path> to FeedUri. The override completes the request by invoking consume with the resource bytes.

Save data

Cross delegates persistence to the platform adapter. The adapter transforms its native storage into streams and passes those streams to the application:

void application::Restore(std::istream& input, Completion completion);
void application::Checkpoint(std::ostream& output);

An empty input stream means that there is no saved snapshot. The adapter keeps the restore stream alive until the application invokes completion. The application must not retain the checkpoint output after Checkpoint returns; the adapter commits it to native storage afterward. If the application sets failbit on the output, the adapter discards the attempted checkpoint and preserves the existing native storage.

Runtime contract

Platform lifecycle                  Runtime and application callbacks

Begin ----------------------------> bridge::Restore(completion) [once]
                                    application::Restore(input, completion)
Create + Start + restored --------> application::CreateStage()
                                    Stage::Attach()
                                    Stage::Resume()

Stop -----------------------------> Stage::Suspend()
                                    bridge::Checkpoint()
                                    application::Checkpoint(output)
Start (warm) ---------------------> Stage::Resume() [same stage]

Destroy --------------------------> Stage::Suspend() [if active]
                                    bridge::Checkpoint() [if needed]
                                    application::Checkpoint(output)
                                    Stage::Detach()

End ------------------------------> Stage::Suspend() [if active]
                                    bridge::Checkpoint() [if needed]
                                    application::Checkpoint(output)
                                    Stage::Detach() [if attached]
                                    destroy Stage

Stage::RequestStage() ------------> suspend, detach, and destroy old Stage
                                    application::CreateStage()
                                    bridge::Checkpoint()
                                    application::Checkpoint(output)
                                    attach and resume as requested
  • Platform adapters exclusively call Begin, End, Create, Destroy, Start, and Stop. Application code, including stages and persistence callbacks, does not call those APIs or own an active Stage.
  • Cross restores application state once per process, before it creates the first stage. It checkpoints after suspending an active stage and after an explicit stage replacement. A warm Stop/Start retains the same stage.
  • Restore may finish asynchronously. It must invoke its completion exactly once on the same serialized owner thread that called Cross. Its input remains valid through that invocation.
  • Checkpoint is synchronous. It must capture and submit a coherent snapshot before returning and must not retain its output; Cross may detach or destroy the current stage immediately afterward.
  • Every lifecycle, dispatch, and completion entry into Cross runs on that same serialized owner thread. A platform callback originating elsewhere must marshal to the owner thread before entering Cross.
  • Stage::Suspend must quiesce stage-owned workers before returning. Cross keeps the suspended stage alive, but rejects or queues work from inactive or detached views.
  • The current stage may call RequestStage() synchronously from an input callback. Cross defers replacement until that callback returns and assigns a new receiver generation immediately before attaching the replacement.
  • CreateStage synchronously returns a non-null requested stage. Restore re-enters Cross only by invoking its supplied completion. Checkpoint, CreateStage, constructors, destructors, and stage lifecycle hooks do not call Cross APIs. Application callbacks and hooks do not throw.
  • These application-side requirements are preconditions. Cross does not diagnose or recover from developer code that violates them.
  • The runtime currently supports one window or scene. A host must enforce that limit.
Build and output

Each platform repository contains its own CMake project and toolchain integration.

  • iOS produces an ipa file for the App Store.
  • Android produces an aab bundle for Google Play.
  • Linux produces a deb package for Debian-based systems.
  • Windows produces an msi installer.
  • Web produces html, CSS, JavaScript, and wasm files for an HTTP server.