Calling C
A game can call C: a library it ships with, like stb or miniaudio, a platform SDK, or C of its own. extern declares a function written in C, with no body:
extern float Lerp3(float a, float b, float c, float t);It's called like any function, from systems, views, event handlers, methods and other functions, and the call costs what a call between C functions does:
system Blend(mut Mix mix)
{
mix.value = Lerp3(mix.a, mix.b, mix.c, mix.t);
}Names
The C function has the name the extern is written with. When C's name doesn't suit Tide, [NativeName] gives it, and the Tide name can follow Tide's style:
[NativeName("stb_perlin_noise3")]
extern float Noise(float x, float y, float z, int xWrap, int yWrap, int zWrap);A namespace doesn't change the C name: in namespace Terrain;, that's Terrain.Noise in Tide and still stb_perlin_noise3 in C.
An extern's parameters can have default values, and calls can name them: C is passed a default like any argument. Externs can share a name too, but C has one function of a name, so each says which C function it is:
extern float c_scale(float value, float by = 2);
[NativeName("c_mix_floats")]
extern float Mix(float a, float b);
[NativeName("c_mix_ints")]
extern int Mix(int a, int b);Where the C goes
In the game's folder, with nothing to set up:
- Every
.cfile in the folder and its subfolders compiles with the game, with the same determinism flags as the engine. Headers next to them are found as C finds them. - Every
.cpp,.ccand.cxxfile does too, as C++ with no runtime behind it: see C++. - Every prebuilt library there links with the game when it was built for the platform being built for:
.aand.libfiles, and.so,.dlland.dylibones. tide reads each library to tell which platform and CPU it's for, so one folder holds them all, named and placed however you like (for phones, see Android). - Code for one platform only goes in
#ifdef, as in any C.
MyGame/
game.tide
noise.c // #define STB_PERLIN_IMPLEMENTATION, then #include "stb_perlin.h"
stb_perlin.h
steam/
steam_api64.lib // Windows: links in
steam_api64.dll // ...and goes next to the game
libsteam_api.so // Linux
libsteam_api.dylib // macOS
steam_web.c // #ifdef __wasm__: stand-ins, as the web has no Steam
physics/
linux/libjoltc.so // Linux
arm64/libjoltc.so // Android, on phones: goes in the app
x86_64/libjoltc.so // Android, on its emulator.so, .dll and .dylib files are copied next to the built game, where it finds them. On Windows, a .dll links through its import library, the .lib that comes with it.
Windows games build for MinGW, so a static library built with Microsoft's compiler may need Microsoft's C runtime and fail to link: rebuild it with clang or MinGW.
On the web, only C files and WebAssembly libraries define functions. When a function has no definition there, an extern function or one the game's C calls, the web build fails and names it; give it a stand-in inside #ifdef __wasm__. Web games build for wasm32-wasip1-threads, which shares memory between threads, so a WebAssembly library needs building for that target too (clang's --target=wasm32-wasip1-threads), or at least with -matomics -mbulk-memory. tide's own clang can build it, as tide cc, with the C library tide brings: tide cc --target=wasm32-wasip1-threads --sysroot=<tide>/wasi/sysroot, where <tide> is the folder tide version says it's installed in.
tide run builds again when a C or C++ file, header or library changes. The C is part of the game's library, which each build replaces, so whatever C keeps in its own variables starts over at each reload.
C++
.cpp files in the game's folder compile with it as .c files do (.cc and .cxx too), so a library written in C++ can go in as source. Tide still calls C: what the game calls is extern "C".
// counter.cpp
struct Counter
{
int count = 0;
int Next() { return ++count; }
};
static Counter counter;
extern "C" int NextId(void)
{
return counter.Next();
}extern int NextId();Tide brings no C++ runtime, so this is C++ the language, with nothing behind it:
-
No standard library.
<vector>,<string>and the rest aren't there, on any platform. C's headers are, under their C names:<stdint.h>, not<cstdint>. -
No exceptions and no RTTI: no
throwandtry, notypeidanddynamic_cast. -
newanddeleteare the code's to define. A runtime is what defines them, so code that uses them does it itself, overmallocandfree. A class with a virtual destructor usesdelete, and one with a pure virtual function needs__cxa_pure_virtual:#include <stddef.h> #include <stdlib.h> void *operator new(size_t size) { return malloc(size); } void *operator new[](size_t size) { return malloc(size); } void operator delete(void *p) noexcept { free(p); } void operator delete[](void *p) noexcept { free(p); } void operator delete(void *p, size_t) noexcept { free(p); } void operator delete[](void *p, size_t) noexcept { free(p); } extern "C" void __cxa_pure_virtual() { abort(); }Placement new, which
<new>would declare, is one line in a header of the code's own:inline void *operator new(size_t, void *place) noexcept { return place; }.
The rest of the language is there, as C++20: classes, templates, lambdas, virtual functions. A global's constructor runs before the game starts, and again at each reload under tide run, like everything C keeps. A function's static is made the first time through, without the lock a runtime would take: two systems on different threads getting there at once is the game's to get right, like the rest of what C does on threads.
When a build fails for want of the runtime, tide says so after the compiler's error, and what to write instead.
Libraries written to need no runtime, as Dear ImGui is, go in as source. One that needs it comes prebuilt instead: a .dll, .so or .dylib behind a C API has its runtime inside. A static library (.a, .lib) has to be built to need none, as the game's own C++ is (-fno-exceptions -fno-rtti -fno-threadsafe-statics, and nothing of the standard library), and the web only has static ones, so a library that needs the runtime can't be used there.
Android
On Android, the game's .so libraries go in the app with it:
- An app is built for two CPUs, arm64 (phones and tablets) and x86-64 (Android's emulator), so a library comes as a file for each. Where one only exists for phones, give its functions stand-ins for the emulator, inside
#if defined(__ANDROID__) && defined(__x86_64__). - Android's libraries and Linux's are the same kind of file. tide tells them apart by the note Android's NDK puts in every shared library it links, which says the Android it was built for, so both can be in the game's folder.
- In the app, a library goes by the name it was linked with (its soname), or by its file's name when it was linked with none. Android wants that name to be
lib<name>.so. - A static library (
.a) can't say it's Android's: the same C compiles to the same thing for Linux and Android. tide takes it for Linux's, so for Android, build the library as a shared one. - A library written in C++ needs C++'s own library with it: link it in when building yours (
-static-libstdc++), or put the NDK'slibc++_shared.soin the game's folder too. - Newer phones have memory pages of 16 KiB, and a library linked for 4 KiB ones may not load there.
tidewarns about such a library; Google Play requires apps to run on those phones. The NDK links for 16 KiB from r28 on, and older ones with-Wl,-z,max-page-size=16384.
Values across
Extern functions take and return plain data, by value:
| Tide | C |
|---|---|
byte |
uint8_t |
long |
int64_t |
char |
int32_t: the character's code point |
int, float, bool |
int32_t (int), float, bool |
float3, int2, quaternion, float4x4, ... |
tide_float3, tide_int2, ... from tide/math.h |
Color, Rect, Entity, PlayerID |
tide_color, tide_rect, tide_entity, tide_player_id |
| an enum | int32_t, or uint8_t and uint16_t for : byte and : ushort |
| a struct or component | a struct with the same fields, in the same order |
A system that splits its entities across threads gives the entities it spawns temporary handles until it's done (see The schedule), so an Entity C gets from one may be temporary: tide_entity_is_temporary(e), from tide/entity.h, tells. Tide gives the real handle to what the system kept, but not to C, so C that keeps entities should keep real ones.
C often takes pointers. Tide has none, and no pointer arithmetic: the parameter says how a value goes to C, and the call takes its address by itself. Every pointer is only good until C returns.
| Parameter | C gets |
|---|---|
mut T x |
T *: the caller's variable, which C can change |
out T x |
T *: the caller's variable, which C makes (see Out parameters) |
in T x |
const T *: the caller's variable, read-only, with no copy (a copy for a value that's no variable's) |
List<T> xs |
const T *: the list's elements, NULL when it's empty. Pass xs.count too |
mut List<T> xs |
T *: the elements, which C can change, but not how many there are |
out List<T> xs |
tide_list_out *: a list C makes, its elements and how many there are (see Lists C makes) |
string s |
const char *: UTF-8, ending in a zero |
A world keeps a list whose elements take more than 16 KB in pieces, so that changing one element doesn't copy the rest. C gets those elements side by side in a copy, made for the call, and with mut, written back into the list once C returns. Smaller lists, and lists that aren't a world's, go to C as they are.
An extern function can return string from C's const char *: the text is copied, so C can reuse its buffer, and NULL is empty text.
extern float Average(List<float> values, int count);
extern string Describe(in Stats stats);
system Report(Scores scores, mut Summary summary)
{
summary.average = Average(scores.values, scores.values.count);
summary.text = Describe(summary.stats);
}float Average(const float *values, int count);
const char *Describe(const Stats *stats);struct Hit
{
float3 point;
float distance;
}
extern bool RayCast(float3 from, float3 direction, mut Hit hit);#include <stdbool.h>
#include "tide/math.h"
typedef struct Hit
{
tide_float3 point;
float distance;
} Hit;
bool RayCast(tide_float3 from, tide_float3 direction, Hit *hit)
{
// ...
return false;
}Tide's types have no padding the compiler adds, so a C struct with the same fields lines up with them. Structs that hold text or lists can't go to C, and neither can lists of text or T? values.
A struct with a T? field can. In the generated header the field is a struct of its own, named for its type: tide_optional_int, tide_optional_float3, tide_optional_Stats. It holds value, then bool ok, then its padding. When ok is false, C leaves the rest of it zero, as Tide does:
// struct Slot { int? item; }
int32_t SlotItem(const Slot slot)
{
return slot.item.ok ? slot.item.value : -1;
}
void ClearSlot(Slot *slot)
{
slot->item = (tide_optional_int){0};
}C functions can't fail (see Errors): they return what C returns. To turn a C function's error code into an error, check it in a Tide function that fails, and call that.
Out parameters
An extern function has out parameters as a Tide function does, for C that gives a value back through a pointer:
extern bool Raycast(float2 from, float2 to, out Hit hit);
system Aim(mut Turret turret)
{
if (Raycast(turret.position, turret.target, out var hit)) turret.range = hit.distance;
}bool Raycast(tide_float2 from, tide_float2 to, hit *out)
{
...
*out = (hit){.distance = distance};
return true;
}To C it's the pointer a mut parameter is. The difference is Tide's: the call says out, can declare the variable right there, and passes nothing in for C to read. C is trusted to write it. A variable C leaves alone keeps what it held, which for one declared at the call is its type's default.
Text can't be out, as C can't make Tide's text: C returns it. A list can, in its own way:
Lists C makes
With mut List<T>, C changes the elements a list has. With out List<T>, C makes the list: its elements, and how many there are. What the list held isn't passed, so nothing of it is copied for C.
extern int WriteImage(out List<byte> image);
system Save(mut Physics physics)
{
physics.size = WriteImage(out physics.image);
}#include "tide/list.h"
int WriteImage(tide_list_out *image)
{
uint8_t *bytes = tide_list_out_room(image, ImageSize());
const uint32_t written = WriteBytes(bytes);
tide_list_out_count(image, written);
return (int)written;
}tide_list_out_room(out, count)gives memory forcountelements side by side, for C to fill. What's in it is unspecified. Asking again gives memory for the new count, and the earlier one is no longer good, so C that finds it needs more asks again and starts over. It's NULL for no elements, and for more than a list holds (1 GiB of them).tide_list_out_count(out, count)says how many of those elements, from the first, the list has once C returns: at most the room last asked for. Never called, the list is empty.
Once C returns, the list is exactly those elements, however many it had: more, fewer or none. The memory is the engine's, kept from one call to the next, so C neither frees it nor keeps it.
A world's list is written piece by piece, and only the pieces whose bytes differ from what the list had. So a big list that comes out mostly the same costs little: the pieces that didn't change aren't hashed again, and stay shared with the snapshots rollback keeps.
C still writes all of it, and all of it is compared. C that knows which elements changed can say only those, and the list keeps the rest:
int PatchImage(tide_list_out *image)
{
tide_list_out_keep(image, ImageSize());
for (int i = 0; i < changed_count; i++) {
tide_list_out_part(image, changed[i].first, changed[i].bytes, changed[i].count);
}
return ImageSize();
}tide_list_out_keep(out, count)says the list keeps the elements it holds, and iscountlong once C returns. The ones past that go, and the ones it gains are zeros.tide_list_out_part(out, first, elements, count)says thecountelements fromfirston are the ones atelements, side by side. Parts are written in the order they're given, a later one over an earlier one, and what's past the list's count is left out.
Nothing of what the list keeps is written, compared or hashed again: only the parts are, each with the elements it goes over. So a list of megabytes that changes in a few places costs those places.
A part's elements are read once C has returned. They have to be in memory that's still good then, and that nothing changes meanwhile: C's own (static, or allocated), never the function's variables. Of keeping and asking for room, what C said last stands, so C that starts out keeping and finds everything changed asks for room and fills it.
The argument is a list the code can change, with out before it, as for any out parameter: a field of a mut component or singleton, a mut local, or one declared there with out var. Its elements are what a list goes to C with.
Systems written in C
An extern function is called for one entity at a time: a system written in Tide takes the components, and passes C what it reads of them. C that wants every entity's data at once (a physics library that reads its bodies back, a loop written with SIMD) would pay a call and a copy of each component for every entity. A system can be written in C instead, and handed the components as the arrays the match keeps them in. It's declared as a system is, with extern before it and no body:
component Position
{
float2 at;
}
component Velocity
{
float2 by;
}
[NativeName("game_move")]
extern system Move(mut Position position, Velocity velocity, Time time, without Frozen);#include <stddef.h>
#include "tide/math.h"
#include "tide/rows.h"
typedef struct Position { tide_float2 at; } Position;
typedef struct Velocity { tide_float2 by; } Velocity;
typedef struct Time { float dt; int32_t tick; float t; int32_t tickRate; } Time;
void game_move(tide_rows *rows, const Time *time)
{
const uint32_t count = tide_rows_count(rows);
float *x = tide_rows_floats_mut(rows, 0, offsetof(Position, at.x));
float *y = tide_rows_floats_mut(rows, 0, offsetof(Position, at.y));
const float *dx = tide_rows_floats(rows, 1, offsetof(Velocity, by.x));
const float *dy = tide_rows_floats(rows, 1, offsetof(Velocity, by.y));
for (uint32_t i = 0; i < count; i++) {
x[i] += dx[i] * time->dt;
y[i] += dy[i] * time->dt;
}
}To the rest of the game it's the system its declaration says. It runs for the entities its parameters pick, it takes [Stage], [Before] and [After], and the schedule has it wait for the systems it shares data with, and they for it, by what it declares: what it takes mut is written, and the rest is read. tide schedule shows it as Move (per entity, in C), and the editors' line above it says in C. Nothing warns about a parameter it doesn't use, as its code can't be read.
What C is called with
C is called once for each run of rows: entities that are side by side in the match's memory. That's a chunk of the entities that have one combination of components, a couple of thousand at most, or a piece of a chunk when the tick shares one out over threads. The runs come in the order a system written in Tide would go through the entities on one thread.
rowscomes first:tide_rows_count(rows)is how many rows the run has, and every array has that many values.- The singletons the system takes follow it, in the declaration's order, each a pointer:
constfor one it reads, and the match's own for one it takesmut.Timeis{ float dt; int32_t tick; float t; int32_t tickRate; }, with anint32_t seedafter them in a game that uses the match's seed. - The components are reached through
rows, by number: 0 for the system's first component parameter, 1 for the second. Filters and singletons don't count.
A system with no component and no filter runs once a tick, as in Tide: C is called once, with no rows (tide_rows_count is 0) and its singletons, which is a way to hand C a singleton.
Fields as arrays
The match keeps a component 4 bytes at a time, each a column of its own, so a field of 4 bytes is already an array, a value for each row. C names a field by where it starts in a struct that has the component's fields in order, with offsetof:
| Call | Gives |
|---|---|
tide_rows_floats(rows, component, offset) |
const float *: a float field, or one part of a vector (offsetof(Position, at.y)) |
tide_rows_ints(rows, component, offset) |
const int32_t *: an int field, an enum stored as an int, half of a long or of an Entity |
tide_rows_floats_mut, tide_rows_ints_mut |
The same to write, for a component the system takes as mut |
tide_rows_entities(rows) |
const tide_entity *: each row's entity |
tide_rows_get(rows, component, row, &value) |
One row's whole component, copied into a struct |
tide_rows_set(rows, component, row, &value) |
The same back, for a mut component |
tide_rows_get and tide_rows_set are for the fields that aren't arrays of 4 bytes (a bool or a byte is one byte of a column, a long and an Entity are two columns), for a component whose size isn't a multiple of 4, which is kept otherwise, and for C that would rather work with a struct at a time.
Nothing is copied to read: an array is the match's own memory. Writing is asked for a field at a time, and costs what's written:
- The match shares what a tick doesn't change with its snapshots, and a client keeps one of every tick it predicts. Asking for an array to write is what makes that field's memory the match's own to change. C that asks for 2 of a component's 20 fields touches those 2.
- A field that comes out as it was is shared again once C returns. So is a row
tide_rows_setputs back as it was read: it writes the columns that differ, and asks for no other. - An array to write has what's there in it, and is the field from then on: read through it too. An array of the same field that was asked for to read before it may no longer be the one the run writes.
- Every array is good until C returns, and no longer.
A field that holds text, a list, a map, a set or a grid is a handle into the match's memory: C leaves those as they are.
A call that can't be answered stops the program, saying which function it was, in which system, and what to write instead: a component the system doesn't take, an offset that isn't a 4-byte field's, an array to write of a component that isn't mut.
What it can take, and do
A system written in C takes components (to read, or mut), filters (with, without) and singletons. The rest is an error that says what to write instead, for now: a sparse component or a filter by one (which rows have it is told row by row, and C gets whole runs), the World, a query, Devices and the input. A system written in Tide takes those, and can call C for each entity.
C can't spawn, destroy, add or remove components, or send events from a system: a system written in Tide does that, from what C left in a component or a singleton.
Threads
A system written in C splits its entities across threads by the rule every system does (see Splitting across threads): its runs go to different threads, and C is called on several at once, unless it takes a singleton as mut. Each run only reaches its own rows, so C that works row by row needs nothing more. C that keeps something from one run to the next, or needs its runs in order, takes a mut singleton, which keeps it on one thread, as it does a system written in Tide.
The engine can't tell how much work C does, so it counts a row of a system written in C as a few calls' worth (256 operations) when it decides whether a tick is worth threads.
Drawing from C
Some libraries draw their own way: Dear ImGui, Nuklear, Clay, RmlUi and Spine hand over triangles every frame, as vertices, indices, and batches that each have a texture and a clip rectangle. How many there are changes every frame, so they don't go through Tide lists: a view passes C its draw list, and C draws into it with the engine's own functions, tide/draw.h.
extern void DrawUI(DrawList list);
view UI()
{
Draw.SetScreen();
DrawUI(Draw.list);
}#include "tide/draw.h"
void DrawUI(tide_draw_list *list)
{
// The batches' vertices, once, then each batch's triangles with its clip
// and its texture
const uint32_t base = tide_draw_vertices(list, vertices, vertex_count);
for (int i = 0; i < batch_count; i++) {
tide_draw_set_clip(list, batches[i].clip);
tide_draw_triangles(list, base, indices + batches[i].first, batches[i].count, &atlas, TIDE_FILTER_BILINEAR);
}
tide_draw_unset_clip(list);
}A DrawList parameter is for extern functions only, and takes Draw.list, which is only ever passed on. Like Draw, it needs the frame, so views and the functions they call can use it. What C draws lands in order with what the view draws before and after the call.
C has everything Tide's drawing APIs have, as tide_draw_circle, tide_draw_text, tide_draw_set_camera, tide_draw_set_screen and the rest, and meshes:
-
tide_vertexis Tide'sVertex: a position, a uv and a color of four floats. Indices areuint32_t. -
tide_draw_mesh(list, vertices, vertex_count, indices, index_count, texture, filter)draws triangles, likeDraw.Mesh.textureis NULL for none.tide_draw_mesh_wrappedtakes atide_wrapafter the filter, for what a uv past the texture's edges reads:TIDE_WRAP_REPEATorTIDE_WRAP_MIRROR, wheretide_draw_meshreads the pixels at the edge (TIDE_WRAP_CLAMP). -
tide_draw_verticesandtide_draw_trianglesare the same in two steps, for vertices several batches share: the first copies them into the list and returns where they start, and the second draws triangles of them.tide_draw_triangles_wrappedtakes a wrap too. -
The draw settings have their two forms in C too.
tide_draw_set_clip(list, rect)isDraw.SetClip(rect), andtide_draw_unset_clip(list)isDraw.SetClip(); the others aretide_draw_set_blend,tide_draw_set_tint,tide_draw_set_depth,tide_draw_set_cull,tide_draw_set_stencil(which takes atide_stencil:tide_stencil_tested(test, value)ortide_stencil_stored(test, value, operation)),tide_draw_set_transform,tide_draw_set_cameraandtide_draw_set_screen, each with itstide_draw_unset_. A block is two calls:tide_draw_push_clip(list, rect)(andtide_draw_push_blend,tide_draw_push_tint,tide_draw_push_depth,tide_draw_push_cull,tide_draw_push_stencil...) returns a mark, andtide_draw_pop(list, mark)puts back what it found, along with whatever was pushed since:cconst uint32_t mark = tide_draw_push_clip(list, panel); tide_draw_push_tint(list, faded); draw_rows(list); tide_draw_pop(list, mark); // BothWhat C sets on the view's list stays set for the view, like a
Setin Tide, so a library that draws through aDrawListpushes what it needs and pops it before it returns. -
tide_draw_text(list, text, position, size, color)isDraw.Textat its top left corner in the regular weight, andtide_draw_text_at(list, text, position, size, color, anchor, font)takes an anchor and a weight: atide_anchor(TIDE_ANCHOR_MIDDLE_CENTER) andTIDE_FONT_REGULARorTIDE_FONT_BOLD. C names a weight by its font file, so a program whose C saysTIDE_FONT_BOLDcarries the bold weight's file, as a game that saysFont.Bolddoes. -
The smooth shapes are
tide_draw_round_rect,tide_draw_ellipse,tide_draw_arcand the rest, with_rotatedfor the turned ones andtide_draw_thick_line,tide_draw_thick_wire_rectandtide_draw_thick_wire_circlefor the ones that take a width.tide_shapeis Tide'sShape, fortide_draw_shape(list, shape), withoutline_colorforoutlineColor. Start one fromTIDE_SHAPE_WHITErather than from zeros: itscolor2isTIDE_COLOR_NONE, Tide'sColor.none, where a zeroed one is a clear color the fill would fade to. -
tide_vertex3is Tide'sVertex3.tide_draw_set_camera_3d(list, position, rotation, field_of_view)andtide_draw_set_camera_matrices(list, transform, projection)areDraw.SetCamera. -
tide_draw_mesh_3d(list, &mesh, transform, texture, filter)draws a 3D mesh, atide_mesh: its vertices and indices, and a version (see below). Instances of one mesh drawn in a row, with one texture, filter and wrap, are drawn at once.tide_draw_mesh_3d_wrappedtakes atide_wrapafter the filter. Its triangles are drawn from their front, the side their corners go clockwise on, unlesstide_draw_set_cullsays otherwise (see Which side draws).
C's own pixels
C draws with pixels it keeps itself, where a library made them:
static tide_texture atlas; // Its pixels: 4 bytes each, red, green, blue and alpha, rows from the first
void LoadFont(void)
{
atlas.pixels = BakeFont(&atlas.width, &atlas.height);
atlas.version++;
}Nothing is created or uploaded, and C holds no handle. A mesh names its pixels, and the draw list copies them when it hasn't got them as they are: when they're new to it, or their version or size isn't what it last copied. The GPU gets them from that copy, and lets them go once a frame draws without them. So:
- Change
versionwhenever the pixels change, or when other pixels come to be at that address. Counting up does it;tide_texture_version(pixels, width, height)hashes them instead, for pixels that rarely change. - C can free its pixels, or change them, as soon as the call returns.
- At a hot reload, C's state starts over, and the draw list forgets what it copied: an atlas the new build bakes is drawn as baked, whatever its address and version.
3D meshes go the same way: a tide_mesh names its vertices and indices where C keeps them, and the list copies them when it hasn't got its version of them, so a mesh that doesn't change costs nothing to draw again, frame after frame. Change version whenever they change, or hash them with tide_mesh_version(&mesh).
A network from C
A game's C can also carry the game's matches: tide/network.h registers a network of the game's own, which Session.Open() and Session.Join() then go through. See A game's own network.
Order
C can keep state, so calling it is a side effect, and Tide keeps its order: in Pick(Roll(), Roll()), the first Roll runs first on every platform, though C itself would let each compiler pick. The same goes for functions, methods and operators that call C, and for calls inside &&, || and ?:, whose parts still only run when they would.
A C function that changes what it's given as mut, or makes a list as out, keeps its turn among what's read beside it too, like any function that does: in Pack(counter.n, c_next(counter)), counter.n is read first.
What C is trusted with
The compiler doesn't look inside C. It takes each call as touching nothing it tracks, so C never makes systems wait for each other, and anything C does is the game's to get right:
- Determinism. Match code runs the same on every machine only if its C does too: no platform math library (
sinf,powf), and nothing that depends on the machine. The game's own C and C++ files get the engine's flags, which keep float math exact; a prebuilt library's flags are whatever it was built with. - State. Variables C keeps aren't in the world, so they aren't sent, rolled back or hashed. Keep what the match depends on in components and singletons.
- Threads. Once systems run in parallel, two that call the same C function can run at the same time.
C also changes how players join. When starting a match calls no C (no match event handler does, through anything it calls), a player joining a big world starts the match on its own machine too, and only gets what changed since (see Sending worlds). C called while a match starts could do something outside the world on every joining machine, so a game whose match start calls C sends joining players the whole world instead.
tidec declares each extern function from its Tide signature, and each extern system's function from its declaration. If it doesn't match the C function, the call goes wrong the way it would in C.