CrossByte is a cross-platform Haxe framework for networked, event-driven, and systems-oriented applications.
It is built for projects that want a strong runtime foundation without dragging in a giant engine shape: sockets, HTTP, RPC, timers, workers, files, crypto, compression, IPC, and a set of practical data structures all live in one coherent core.
CrossByte aims to stay modular. The core provides portable behavior first, while optional sibling haxelibs can add native-backed integrations when they are worth the extra dependency.
haxelib install crossbyteOr track the repository:
haxelib git crossbyte https://github.com/dimensionscapeorg/crossbyte.gitNative (hxcpp) builds need the production branch of the dimensionscape/hxcpp fork:
haxelib git hxcpp https://github.com/dimensionscape/hxcpp.git productionIt carries the socket, poll and TLS corrections CrossByte depends on, ALPN for HTTP/2, TLS session resumption and the bundled MySQL client, and it exposes hxcpp's mbedTLS to libraries, which CrossByte's RSA and ECDSA build against. hxcpp 4.3.2, the release on haxelib, has none of these and cannot build CrossByte's crypto. CI builds against the same branch.
The JVM, Node, HashLink, Neko and the interpreter need nothing else to install; Node needs hxnodejs. What a target cannot do it says at the member: the interpreter, for one, cannot start a child process (NativeProcess.isSupported is false there).
Install the published CrossByte task runner with:
haxelib install aedifex- evented applications and services
- TCP, WebSocket, and reliable datagram networking
- peer-to-peer connections through NAT, including WebRTC data channels to and from browsers
- HTTP clients and lightweight HTTP server flows, HTTP/1.1 and HTTP/2
- request/response RPC over live connections
- file, byte, and stream-heavy workflows
- headless runtimes, tools, and backend infrastructure
- cross-target foundations that still leave room for native extensions
CrossByte currently includes:
- async runtime and timer scheduling
- event system and typed event classes
- HTTP and middleware
- URL loading and request utilities
- TCP, WebSocket, and RUDP transport layers
- NAT traversal and WebRTC:
PeerConnectionandDataChannel-- the full stack, ICE to SCTP over DTLS, interoperable with a browser'sRTCPeerConnectionin either signalling direction (CI proves both against headless Chrome)StunClientand per-connection reflexive gathering, so a peer behind NAT can learn the address the world sees, and RFC 5780's tests (classifyMapping,classifyFiltering) for what kind of NAT is in the wayTurnClientand relayed candidates for the peers no direct path reaches, verified in CI against an independent TURN server- hole punching on the reliable datagram sockets, for the same problem without the browser, with a TURN relay to fall back on where punching fails (
ReliableDatagramServerSocket.allocateRelay)
- RPC sessions, commands, handlers, and typed responses -- see the RPC guide
- IPC primitives such as
LocalConnection,SharedChannel, andSharedObject, natively (cpp) on Windows, Linux and macOS; on every other target they say so withisSupportedand throw when used - file APIs,
ByteArray,ByteArrayInput, andByteArrayOutput - compression:
- DEFLATE
- GZIP
- LZ4
- Brotli
- crypto:
- natively (cpp) only, from libsodium and BLAKE3 compiled in:
Aead(XChaCha20-Poly1305),KeyExchangeandX25519,GenericHash(BLAKE2b),HKDF,Ed25519andBlake3; and from mbedTLS, RSA and ECDSA signatures (PublicKeySignature,SignatureKey). ElsewhereisAvailable()is false and they throw Argon2idnatively and on Node 24.7 or later;BCrypteverywhere, in Haxe- secure random bytes natively, on the jvm, on Node, in a browser and on PHP; not on the interpreter, neko or HashLink
- natively (cpp) only, from libsodium and BLAKE3 compiled in:
- workers, task pools, and
NativeProcess, which starts a child process and reads its output natively, on the jvm, HashLink, Neko and Node; not on the interpreter, whose process calls hold every thread while they wait, nor in a browser - data structures and utility packages
ExpiringMap, for what a server keeps on a peer's behalf -- sessions, tokens, pending handshakes -- bounded by time (ttl) and by count (maxSize, 100,000 unless set), andObjectPool, which keeps 10,000 free objects unlessmaxFreesays otherwise, so a burst does not stay in memory for good
- database surfaces for:
- SQLite, natively, its statements prepared once and their parameters bound
- every SQL driver's statements take
parametersasSQLValues, and read a result row by row and column by column withexecuteEachandSQLRow - MySQL and MariaDB: natively through hxcpp's bundled client, which logs in with
caching_sha2_passwordormysql_native_password, uses TLS when the server offers it (MySQLConfig.sslMode), bounds its waits and cancancel()a statement; on the jvm through Connector/J on the class path, without the TLS or limit settings- natively the client takes the server's answers as untrusted, since whoever answers in the server's place writes them, and refuses one no server sends -- a column count or a length that would otherwise have it allocate gigabytes or write out of bounds -- with error 2027 (
MySQLConnectionlists what it bounds).sslModestaysPREFERRED, as in MySQL's own clients, which encrypts without checking whose certificate it is: across a network you do not trust, useVERIFY_IDENTITYwithsslCa(MySQLConfig.sslModesays why insisting on TLS alone would not keep out a man in the middle)
- natively the client takes the server's answers as untrusted, since whoever answers in the server's place writes them, and refuses one no server sends -- a column count or a length that would otherwise have it allocate gigabytes or write out of bounds -- with error 2027 (
- PostgreSQL: natively through libpq, loaded at run time, with bound parameters, statement and connect timeouts and
cancel(); on php through PDO; no other target - MongoDB, through its wire protocol (OP_MSG, SCRAM, TLS, cursors, transactions) on hxcpp, the jvm, the interpreter, hl and neko; not on JavaScript, which cannot block
ConnectionPoolandAsyncDatabasekeep those blocking drivers off the runtime's thread, and bound the wait:AsyncDatabasefails a job still queued afterqueueTimeout(30 s) -- at the deadline, though every worker is busy -- and refuses one pastmaxQueued(100,000); 0 is no limit for either, and for the pool'sacquireTimeout- natively the MySQL, PostgreSQL and MongoDB clients keep TCP keepalive on with MySQL's timings (a probe after 60 idle seconds, then every 10, dropped after 6), so a database host gone silent -- a partition, a crash -- is found in about two minutes rather than hours or never; MongoDB's does on the jvm too, with the timings from Java 11, and cannot on the interpreter, hl and neko (
MongoConfig.keepAlivesays what each target can)
Every CrossByte runtime schedules its timers with a min-heap, and for almost everything that is the end of the story. It orders timers exactly, a one-millisecond delay costs what a six-hour delay costs, and thirty thousand recurring timers still leave it using well under a millisecond per frame. You should not have to think about it.
The exception is a runtime holding thousands of short timers that it re-arms
constantly — a deadline per connection, a cooldown per entity. That is where
the heap's O(log n) starts to show, and CrossByte ships a timing wheel for
it:
class MyServer extends ServerApplication {
public function new() {
super(WHEEL);
}
}The choice belongs to the runtime rather than the build, because a process usually has more than one and they rarely want the same answer. A simulation thread carrying a timer per entity and a network thread carrying a handful can each have what suits them:
var sim = CrossByte.make(DEFAULT, WHEEL);
var net = CrossByte.make(POLL, HEAP);Measured with one recurring timer per entity, CPU spent per simulated second at sixty ticks:
| timers | heap | wheel |
|---|---|---|
| 1,000 | 1ms | under 1ms |
| 10,000 | 10ms | 2ms |
| 30,000 | 44ms | 7ms |
Arming and cancelling is roughly twice as fast.
Before you switch, the other side of it. The wheel covers a fixed span ahead of now, and anything scheduled past that span waits in a list it rescans periodically — so if your timers are mostly long, you are paying for work the heap never does, and you should stay on the heap. Two smaller differences: timers due in the same tick fire in bucket order rather than by exact time, and a timer can be late by up to a tick. It will never be early; that one is guaranteed.
The short version: reach for the wheel when you have actually seen the scheduler in a profile and your timers are numerous and short. Otherwise the default is already the right answer.
A runtime runs on one thread. Everything a server does -- its sockets, its
handlers, its timers -- runs on its runtime's thread, so a server on one
runtime uses one core however many the machine has. CrossByte.make makes a
runtime per thread, and a server can spread its connections over several:
var server = new ServerSocket();
// Four runtimes, each a thread of its own polling its sockets.
server.runtimes = [for (_ in 0...4) CrossByte.make(POLL)];
server.addEventListener(ServerSocketConnectEvent.CONNECT, function(event) {
// Runs on the runtime this connection was handed to, and so does
// everything the connection does from here on.
var socket = event.socket;
socket.addEventListener(ProgressEvent.SOCKET_DATA, _ -> {
socket.writeUTFBytes(socket.readUTFBytes(socket.bytesAvailable));
socket.flush();
});
});
server.bind(9000);
server.listen();The listener stays on the runtime that called listen() and accepts. Each
connection it accepts is handed, before its TLS handshake, to one of
runtimes -- each in turn, passing over one that has exited -- and is that
runtime's for its whole life: its socket is polled there, its events and
deadlines run there, and so does the connect listener that receives it.
runtimeCount = 4 makes the runtimes instead. ServerWebSocket takes the
same, and so does HTTPServer, through its configuration:
var config = new HTTPServerConfig("0.0.0.0", 8080);
config.runtimeCount = 4;
config.middleware.push(router.middleware());
var server = new HTTPServer(config);Each request is served on its connection's runtime, HTTP/1.1 and HTTP/2
alike. maxConnections, the rate limiter and the metrics count every
runtime's connections together, and drain(), close() and
stopAccepting() cover all of them.
What it buys, measured natively on Windows with small GETs from 64
kept-alive connections, the server held to eight logical CPUs and the
clients to eight others (tests/scaling):
| runtimes | HTTP/1.1 requests/s | HTTP/2 requests/s |
|---|---|---|
| one (not spread) | 78,000 | 75,000 |
| 2 | 178,000 | 154,000 |
| 4 | 312,000 | 312,000 |
A server on one runtime pays nothing for the feature: the same server built before it measured the same.
selectRuntime chooses the runtime instead, on the listener's runtime, from
the peer's address. A game server that runs each match on a runtime of its
own sends a player to the runtime that owns their match, so the match's
state is only ever touched from one thread:
// What the matchmaker decided: the runtime each joining address's match runs on.
var joining:Map<String, CrossByte> = new Map();
var server = new ServerWebSocket();
server.runtimes = matchRuntimes;
server.selectRuntime = (address, port) -> joining.get(address);
server.addEventListener(ServerSocketConnectEvent.CONNECT, function(event) {
// On the match's runtime: the match can be reached without a lock.
matchOn(CrossByte.current()).join(cast event.socket);
});An answer of null, or of a runtime that has exited, takes the next in
turn.
What runs where, and what it must be. Handlers that keep to their own
connection need nothing. What several runtimes' handlers share -- a table of
players, a cache, a counter, a database pool -- is touched from several
threads at once, and must be thread-safe or kept per runtime: reach the
runtime's own with CrossByte.current(), or hand work to one with
runtime.post(...), the one thread-safe way into a runtime. In particular:
connectlisteners, anHTTPServer's middleware, routes and hooks (onError,onExpectContinue,rateLimitKey), aServerWebSocket'supgradeand an SNI predicate run on each connection's runtime, several at once;admitandselectRuntimerun on the listener's runtime alone;- a
Routeris read-only once its routes are added, and safe to share; - the server's own shared pieces are made safe for you: the limits and
counts,
HTTPServerConfig.rateLimiter(given a lock as the server starts), the metrics registry and the compression cache.
Add listeners and routes before listen(); close() on a connection from
any thread is handed to its runtime, as everywhere.
On Linux, reusePort gives each runtime a listening socket of its own on
the port (SO_REUSEPORT) and lets the kernel share connections out, so no
one runtime accepts for the others -- worth it when connections arrive faster
than one thread accepts them. The kernel then decides where a connection
goes, so selectRuntime is not asked, and each runtime asks admit for
itself. macOS and the BSDs take the option without sharing anything out, and
Windows has nothing like it; setting it there throws.
Where it works. Natively and on the jvm each runtime is a thread and they
run at once. neko's threads run at once too but contend for its allocator:
the server above, on neko, answered 1.4 times as many requests on two
runtimes as on one, and no more on four. hl runs them on threads as well. On
the interpreter the runtimes take turns -- two busy threads take twice as
long as one -- so a spread server is served correctly and no faster. On Node
every runtime shares one thread, so a spread server is refused; run several
processes there (Node's cluster).
When several processes are better. Every runtime in a process shares one
garbage collector, and a collection stops all of them at once: at high
allocation rates the pauses, not the cores, bound the throughput, and a
latency-sensitive server sees every runtime's pause. A process also fails as
a whole -- one handler's crash, one leak, takes every runtime down -- and
shares one memory budget. Several processes behind a load balancer -- or, on
Linux, a server in each with reusePort set, all on one port -- give each
its own collector and its own fate, at the price of sharing nothing without
a network hop. Spread a process over
runtimes when connections need to reach shared state cheaply (a game world, a
cache); use processes when they do not.
What a server you call can cost you is bounded per request, on the
URLRequest, so one caller's settings never reach another's requests:
URLRequest |
Default | Bounds |
|---|---|---|
idleTimeout |
30 s | time with nothing arriving -- and, off JavaScript, the wait for a load thread past URLLoader.maxConcurrentLoads |
headTimeout |
300 s | the response's head, from the request having gone; bytes trickling in do not move it |
totalTimeout |
none | the whole load, from load() to COMPLETE |
maxBodySize |
64 MB | the body on the wire; a larger Content-Length is refused before it is read |
maxDecompressedSize |
64 MB | what the body decodes to |
maxResponseHeaderSize |
64 KB | the status line and header fields, 1xx responses included |
maxRedirects |
10 | redirects followed before the load fails |
0 or less lifts any of them. An idle timeout alone does not bound a request:
a server sending a byte at a time resets it with every byte, so give a
request to a server you do not trust a totalTimeout:
var request = new URLRequest("https://api.example.com/report");
request.totalTimeout = 60000;
request.maxBodySize = 4 * 1024 * 1024;
var loader = new URLLoader();
loader.addEventListener(IOErrorEvent.IO_ERROR, (e:IOErrorEvent) -> trace(e.text));
loader.load(request);A custom HTTPBackend reads the same limits from its HTTPRequestContext;
the loader cancels a request past its totalTimeout through the context's
cancelToken.
Names. A socket's connect by name, and every client request, looks the name up on one of four threads the process keeps for it -- a system lookup cannot be stopped, so that is the most a wedged resolver can hold -- and its caller waits 30 s for the answer at most (less within a request's idle timeout, and a cancel ends the wait at once). An answer is kept for 30 s, a failure for 5, and callers asking for a name already being looked up share the lookup.
PHP. HTTPServerConfig.phpMaxResponseSize (8 MiB) bounds a script's
response, whose CGI header block is held to 64 KiB and 100 lines besides,
and phpMaxExchanges (64) the requests a runtime has with its PHP backend
at once; more wait their turn within phpTimeout, and past 1,024 waiting
a request is refused.
CrossByte's extension story is intentional: features that benefit from native backends or external platform libraries can live in sibling haxelibs instead of bloating the core.
Current extension repos:
crossbyte-libuv- native libuv-backed poll backend
crossbyte-brotli- native Brotli backend
crossbyte-lz4- native LZ4 backend
The core remains usable without these extensions. When installed, they can be enabled selectively for native-backed behavior where it matters.
All optional, all off unless you pass them.
| Define | Effect |
|---|---|
crossbyte_brotli_native |
Route Brotli through the native backend from the crossbyte-brotli haxelib instead of the bundled Haxe implementation. |
crossbyte_lz4_native |
Route LZ4 through the native backend from the crossbyte-lz4 haxelib instead of the bundled Haxe implementation. |
crossbyte_libuv_native |
Build the libuv poll backend from the crossbyte-libuv haxelib (cpp only). Needs libuv's headers and library, and LibuvPoll.install() called before the first runtime is created; without the define install() returns false and the built-in backend is used. See that repository's README. |
crossbyte_no_http2 |
Do not auto-register the bundled HTTP/2 backend. A backend registered explicitly through HTTPBackendRegistry still wins either way; this only stops the bundled one from being picked up on its own. |
http_debug |
Log each response line the HTTP client reads, through Logger, so it honours the configured level and sink. |
crossbyte_debug |
Keep crossbyte.io.File out of @:noDebug, so its frames appear in stack traces. |
For example:
haxe -lib crossbyte -lib crossbyte-lz4 -D crossbyte_lz4_native -main Main --cpp bin
One of hxcpp's own matters to a server that holds a lot in memory. hxcpp's
collector numbers its 32 KB blocks in two bytes, so a native process can hold
about 2 GB of objects and no more: past that, an allocation stops the process
with an access violation ("Memory exhausted" in a debug log). Build with
-D HXCPP_GC_BIG_BLOCKS for 64 KB blocks and twice that. The collector also
stops every thread while it marks, for a time that grows with what is live --
about 0.2 ms a megabyte of small objects, measured by the load harness's game
server at sixty ticks a second: pauses of 50-70 ms every few seconds with
300 MB live, about 200 ms every twenty seconds with 1.1 GB, up to 0.7 s with
2.4 GB. A runtime whose ticks must stay inside a frame wants its live heap
well under 100 MB, or the world held where the collector does not scan it.
Where a collection fits between two ticks, CrossByte.collectWhenIdle moves
it there: the runtime learns how often the collector runs and makes the
collection that is due in the gap before a tick instead. At thirty ticks a
second with 180 MB of small objects live, two collections in three left the
ticks for the gaps between them; a collection longer than two thirds of the
gap stays where it falls.
Both build and run the test suite, in CI on Windows and Linux. What they need, and what they do not have:
HashLink 1.13 or later, and say so. Haxe 4.3 assumes HashLink 1.12 unless
told otherwise, and crossbyte.utils.Random uses haxe.atomic, which will not
compile for anything older -- the build stops inside the standard library with
"Atomic operations require HL 1.13+". Pass the version you run on:
haxe -lib crossbyte -D hl-ver=1.13.0 -main Main --hl main.hl
The .hdll files, next to hl. HashLink resolves every native a program
was compiled with when it loads, not when one is called, so a missing library
stops the program before main -- and on Windows it says so in a dialog box,
which on a service or a build machine nobody will ever click. Which ones a
program needs depends on what it compiles in:
| library | needed by |
|---|---|
ssl.hdll |
anything that uses the network, TLS or not: every socket type reaches sys.ssl |
fmt.hdll |
haxe.crypto.Md5 and Sha1 (the WebSocket handshake, TURN credentials) and haxe.zip |
sqlite.hdll |
SQLiteConnection |
mysql.hdll |
MySQLConnection |
The HashLink release for Windows ships all four. A Linux build from source
makes them with make libhl hl fmt ssl sqlite mysql, given mbedTLS, zlib,
libpng, libturbojpeg, libvorbis and SQLite's headers. A library a program
never calls can be skipped with HL_DISABLED_LIBS=sqlite,mysql (HashLink
1.14): its functions then throw when called rather than stopping the load.
What is not there. Neither target has a secure random source, so
SecureRandom.isSupported is false and everything that needs one refuses,
saying so: BCrypt.hash, PKCE, WebSocket clients, STUN, TURN, ICE and WebRTC.
Both are IPv4 only. LocalConnection, SharedChannel and SharedObject and
the libsodium, BLAKE3 and mbedTLS crypto are native features only; ALPN (so
HTTP/2 over TLS) is native or jvm; and a datagram socket's buffers cannot be sized (DatagramSocket.bufferSizeSupported
is false: they read 0 and setting them throws). On Linux, hl polls its sockets
through select, which cannot watch a descriptor numbered 1024 or above, and
hl has no poll natives to move to: a server there fails its polling once that
many descriptors are open. neko polls through its own natives and is not held
to it.
Two things about neko's numbers and clock. An Int there is 31 bits, and
Array.sort is a native merge sort that takes a comparator's answer too large
for one as "less" -- so a comparator written as a subtraction of large values
sorts wrongly there; answer -1, 0 or 1. And on Windows haxe.Timer.stamp() is
the time of day to the millisecond, moving once a system tick, so two readings
a few microseconds apart are usually equal.
The repository includes small runnable samples for:
- primordial applications
- TCP chat
- RPC
- LocalConnection, SharedChannel, and SharedObject IPC
- UDP and reliable datagrams
- HTTP serving
- one HTTP server on several cores (
multicore) - worker/background tasks
See samples/README.md for the current sample index and build commands.
CrossByte uses utest for its test suite.
The repository root is now described by Aedifex.hx. That file is the source of truth for the library identity, task list, and generated haxelib.json metadata.
To refresh haxelib.json from Aedifex.hx, run:
aedifex haxelib sync <project-root>Run the fast interpreted suite with Aedifex:
aedifex task interp-tests <project-root>The raw compiler entrypoint still exists underneath:
haxe ci/interp-tests.hxmlBuild the native smoke executable with Aedifex:
aedifex task native-tests <project-root>The raw compiler entrypoint still exists underneath:
haxe ci/native-tests.hxmlThen run the produced executable:
./export/ci-native-tests/NativeSmokeMainTo inspect the registered CrossByte tasks, run:
aedifex tasks -json <project-root>Generate docs through Aedifex with:
aedifex task docs-api <project-root>
aedifex task docs-site <project-root>In the examples above, <project-root> is usually . when you are already in
the repository root.
A performance suite lives in tests/bench and covers the paths that run once
per unit of real work -- per datagram, per connectivity check, per event -- so
a regression there is a regression multiplied by traffic. Build and run it
natively:
haxe ci/bench.hxml
./export/bench/BenchMain.exeIt reports the best of several samples per case; the numbers compare shapes of code on one machine in one sitting and are not comparable across machines. CI runs it so it cannot rot, and ignores the numbers.
tests/crossbyte/AllocationBudgetTest.hx measures how many bytes each common
operation allocates -- an HTTP/1.1, HTTP/2 and TLS request, a WebSocket and a
TCP echo, a reliable and a plain datagram, an RPC call, an event, a timer, a post
and an idle frame -- and fails when one passes its budget: what it measured when the
budget was set, plus about a quarter. It runs in the native and jvm suites,
the two targets with an allocation counter; AllocationMeter says how each is
read. A failure names the operation, what it allocated in each of three runs,
its budget and the figure the budget was set from. Natively the runs read the
same to the byte, so a failure that repeats when the class runs alone
(-D gc_bisect, CB_ONLY=AllocationBudget) is a path allocating more. To
rebaseline after a deliberate change, run the class alone with
CB_ALLOC_REPORT=1 set, which prints every figure, natively on Windows and on
Linux and on the jvm, and set the measured figures, the budgets and the date
at the top of the class.
tests/load runs CrossByte the way a server and a game server run it -- for
minutes, at scale, with the clients in other processes -- and reports what the
server costs and whether what it holds comes back down once the clients have
gone. Build it natively for the machine you are on, then run a scenario:
haxe ci/load.hxml
./export/load/LoadMain game --clients 1000 --hz 60 --seconds 600
./export/load/LoadMain churn --plan 50:300,200:300,1000:300,0:120
./export/load/LoadMain idle --clients 10000 --seconds 120game: a reliable-UDP game server ticking at--hz, sending every client a 100-400 byte snapshot each tick (sequenced, every fourth reliable) and taking an input from each every tick. Reports processor time per tick, tick time, input-to-acknowledgement latency, lost snapshots and retransmissions, and memory per session. What a session sends waits for its congestion window, up toReliableDatagramSocket.maxOutputBufferSize: 256 KB by default, past which the session is ended with anioErrorsaying why, rather than held without end for a client that has stopped taking it. At 1,000 clients and 60 Hz no session held anything waiting while the server kept its tick, and none more than 18 KB when it was starved of processor time. A game that sends more at once -- a level, one large reliable message -- raises the limit, or sets it to 0 for none.churn: HTTP/1.1 keep-alive, HTTP/2 and WebSocket clients, half over TLS, connecting, doing a few requests and leaving, at each concurrency of--plan(concurrency:seconds,...; end with a0:phase to watch memory come back). Its default clients aretests/load/churn-client.jsand need Node 18 or later, whose TLS connections resume their sessions as browsers' do;--client nativeuses CrossByte's own clients instead, which do not resume and speak HTTP/2 in clear only.idle: that many WebSocket connections held open and quiet: memory per connection, and what holding them costs of a core.
Each prints LOAD {json} lines -- a window every --report seconds, then a
summary -- and exits 0 when every client saw what it should have. Useful
options: --cpus 2-5 --client-cpus 6-15 keeps the server and its clients on
separate processors (a server sharing a core with its own clients measures
far more processor time a tick); game --world-mb 1024 holds that much live
world data, to see the collector's pauses (see Build defines); churn --ops 1000:1000 --think 0 --kinds h1 --tls-share 0 measures one runtime's
throughput for one protocol rather than its churn. On Linux, raise the
descriptor limit before a large idle run (ulimit -n 65536). To run
on crossbyte-libuv's backend, build with that library as its README says
(-lib crossbyte-libuv -D crossbyte_libuv_native, plus LIBUV_INCLUDE,
LIBUV_LIB and LIBUV_STATIC on Windows) and pass --libuv.
ci/load-jvm.hxml builds the game server for the jvm; give it --bots with
the native executable so only the server is the jvm's. Like the soak, CI does
not run any of this: the numbers are for reading, not gating.
The repository CI covers:
- fast interpreter tests
- generated API documentation
- hxcpp API audit builds
- the native suite on Windows, Linux and macOS
- native sample builds
- the whole suite on HashLink and Neko, on Windows and Linux (
hl-neko.yml) - sibling extension jobs for the optional native modules
CI builds against the production branch of the dimensionscape/hxcpp fork, which is also what a local native build should use: the poll/index fixes CrossByte depends on, and the fork's other corrections since. socket-fixes is the narrow branch the upstream pull request lives on; the note in ci.yml says why CI does not follow it.
Each CI run now also publishes a crossbyte-api-docs artifact containing the generated dox site for that revision.
CrossByte is trying to be a serious runtime layer, not a grab-bag of unrelated helpers.
That means:
- portable core behavior first
- native acceleration as opt-in extensions
- efficient hot paths for network and byte-oriented code
- typed APIs where they add real leverage
- enough low-level access to stay useful in unusual projects
If you are building something network-heavy, service-oriented, or systems-adjacent in Haxe, CrossByte is meant to give you a lot of the unglamorous but important foundation work in one place.
