Skip to content

Instantly share code, notes, and snippets.

@rodydavis
Last active November 14, 2025 01:30
Show Gist options
  • Select an option

  • Save rodydavis/d76860eaa9ffba008278ccbf995f1db0 to your computer and use it in GitHub Desktop.

Select an option

Save rodydavis/d76860eaa9ffba008278ccbf995f1db0 to your computer and use it in GitHub Desktop.
Preact Signals in C
#include <stdio.h>
#include <stdlib.h> // For malloc, free
#include <time.h> // For clock_gettime
#include <stdint.h> // For intptr_t
#include "signals.h" // Your signals library
// How many computed signals to chain together
#define CHAIN_DEPTH 100
// How many times to update the head signal
#define NUM_UPDATES 100000
// This global sum is the "observable side effect"
// that prevents the optimizer from skipping the work.
long global_sum = 0;
/**
* A computed function that reads its dependency (ctx),
* adds 1, and returns the new value.
*/
void* compute_chain(void* context) {
Signal* dependency = (Signal*)context;
// Read the dependency's value
intptr_t value = (intptr_t)computed_get_value((Computed*)dependency);
// Return a new value
return (void*)(value + 1);
}
/**
* An effect function that reads the final computed
* value and adds it to the global sum.
*/
CleanupFn effect_sum(void* context) {
Signal* dependency = (Signal*)context;
// Read the dependency's value
intptr_t value = (intptr_t)computed_get_value((Computed*)dependency);
// Produce the side effect
global_sum += (long)value;
return NULL;
}
int main() {
printf("Running update benchmark...\n");
printf(" Graph: 1 Signal -> %d Computeds -> 1 Effect\n", CHAIN_DEPTH);
printf(" Updates: %d\n", NUM_UPDATES);
printf("--------------------------------------------\n");
// --- 1. Build the graph ---
// We need to store all the computed signals to free them later
Computed** computeds = malloc(sizeof(Computed*) * CHAIN_DEPTH);
if (computeds == NULL) {
fprintf(stderr, "Failed to alloc storage\n");
return 1;
}
// 1a. Create the source signal
Signal* source = signal_create((void*)(intptr_t)0, "source", NULL, NULL);
// 1b. Create the chain of computed signals
Signal* last_node = source;
for (int i = 0; i < CHAIN_DEPTH; i++) {
computeds[i] = computed_create(compute_chain, last_node, "computed", NULL, NULL);
// The new computed becomes the dependency for the next loop iteration
last_node = &computeds[i]->base;
}
// 1c. Create the final effect that reads the end of the chain
// The initial run will add 0+100 (100) to global_sum
Effect* effect = effect_create(effect_sum, last_node, "effect");
printf("Graph built. Initial sum: %ld\n", global_sum); // Should be 100
// --- 2. Run the update benchmark ---
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 1; i <= NUM_UPDATES; i++) {
// Update the source signal. This will cause all 100
// computeds to be marked as dirty and the effect to re-run.
signal_set_value(source, (void*)(intptr_t)i);
}
clock_gettime(CLOCK_MONOTONIC, &end);
// --- 3. Print results ---
double time_sec = (end.tv_sec - start.tv_sec) +
(end.tv_nsec - start.tv_nsec) / 1e9;
printf("Benchmark finished.\n");
printf(" Total time: %.4f seconds\n", time_sec);
printf(" Avg. update: %.2f us (microseconds)\n", (time_sec / NUM_UPDATES) * 1e6);
printf(" Updates/sec: %.0f\n", NUM_UPDATES / time_sec);
printf("--------------------------------------------\n");
// We print global_sum to *prove* the work was done.
// The compiler CANNOT optimize this away.
printf("Final global sum: %ld\n", global_sum);
// --- 4. Clean up ---
effect_dispose(effect);
// We must free the "downstream" dependents before the "upstream" sources.
for (int i = CHAIN_DEPTH - 1; i >= 0; i--) {
computed_free(computeds[i]);
}
free(computeds);
signal_free(source);
return 0;
}
#include <stdio.h>
#include <stdlib.h> // For malloc, free
#include <time.h> // For clock_gettime
#include <stdint.h> // For intptr_t
#include "signals.h" // Your signals library
// The number of signals to create for each test
const int NUM_ITERATIONS = 1000000;
// A minimal compute function for the computed benchmark
void* simple_compute(void* ctx) {
// We don't even read a dependency. We are just testing
// the overhead of creating the computed struct itself.
return ctx;
}
// A minimal effect function for the effect benchmark
CleanupFn simple_effect(void* ctx) {
// This effect does nothing and has no dependencies.
// We are testing the effect_create() overhead, which
// includes the *first* run of the effect.
(void)ctx;
return NULL;
}
// Helper to get time and print formatted results
void print_results(const char* title, int iterations, struct timespec* start, struct timespec* end) {
double time_sec = (end->tv_sec - start->tv_sec) +
(end->tv_nsec - start->tv_nsec) / 1e9;
printf("%s:\n", title);
printf(" Total time: %.4f seconds\n", time_sec);
printf(" Avg. time/op: %.2f ns\n", (time_sec / iterations) * 1e9);
printf(" Operations/sec: %.0f\n", iterations / time_sec);
}
int main() {
struct timespec start, end;
printf("Running benchmarks for %d iterations...\n", NUM_ITERATIONS);
printf("--------------------------------------------\n");
// --- 1. Benchmark: Plain Signal Creation & Destruction ---
// Allocate storage for all the signal pointers
Signal** all_signals = malloc(sizeof(Signal*) * NUM_ITERATIONS);
if (all_signals == NULL) {
fprintf(stderr, "Failed to alloc storage\n");
return 1;
}
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 0; i < NUM_ITERATIONS; i++) {
// We pass NULL for name/callbacks to test the fastest path
all_signals[i] = signal_create((void*)(intptr_t)i, NULL, NULL, NULL);
}
clock_gettime(CLOCK_MONOTONIC, &end);
print_results("signal_create()", NUM_ITERATIONS, &start, &end);
// Test destruction
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 0; i < NUM_ITERATIONS; i++) {
signal_free(all_signals[i]);
}
clock_gettime(CLOCK_MONOTONIC, &end);
print_results("signal_free()", NUM_ITERATIONS, &start, &end);
free(all_signals); // Free the storage array
printf("--------------------------------------------\n");
// --- 2. Benchmark: Computed Signal Creation & Destruction ---
Computed** all_computeds = malloc(sizeof(Computed*) * NUM_ITERATIONS);
if (all_computeds == NULL) {
fprintf(stderr, "Failed to alloc storage\n");
return 1;
}
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 0; i < NUM_ITERATIONS; i++) {
all_computeds[i] = computed_create(simple_compute, (void*)(intptr_t)i, NULL, NULL, NULL);
}
clock_gettime(CLOCK_MONOTONIC, &end);
print_results("computed_create()", NUM_ITERATIONS, &start, &end);
// Test destruction
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 0; i < NUM_ITERATIONS; i++) {
computed_free(all_computeds[i]);
}
clock_gettime(CLOCK_MONOTONIC, &end);
print_results("computed_free()", NUM_ITERATIONS, &start, &end);
free(all_computeds); // Free the storage array
printf("--------------------------------------------\n");
// --- 3. Benchmark: Effect Creation & Destruction ---
Effect** all_effects = malloc(sizeof(Effect*) * NUM_ITERATIONS);
if (all_effects == NULL) {
fprintf(stderr, "Failed to alloc storage\n");
return 1;
}
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 0; i < NUM_ITERATIONS; i++) {
all_effects[i] = effect_create(simple_effect, (void*)(intptr_t)i, NULL);
}
clock_gettime(CLOCK_MONOTONIC, &end);
print_results("effect_create()", NUM_ITERATIONS, &start, &end);
// Test destruction
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 0; i < NUM_ITERATIONS; i++) {
effect_dispose(all_effects[i]);
}
clock_gettime(CLOCK_MONOTONIC, &end);
print_results("effect_dispose()", NUM_ITERATIONS, &start, &end);
free(all_effects); // Free the storage array
printf("--------------------------------------------\n");
return 0;
}
#include <stdio.h>
#include <stdint.h> // For intptr_t
#include "signals.h" // Import the header
/**
* Function for the computed signal.
* Context will be the 'count' signal.
*/
void* compute_doubled(void* context) {
Signal* count_sig = (Signal*)context;
// Get the value of our dependency
// This is safe because intptr_t is an integer type
// guaranteed to be large enough to hold a pointer.
intptr_t current_count = (intptr_t)signal_get_value(count_sig);
intptr_t new_val = current_count * 2;
printf("... (Computed) Doubling %ld to %ld\n", (long)current_count, (long)new_val);
// Return the new value
return (void*)new_val;
}
/**
* Function for the effect.
* Context will be the 'doubled' signal.
*/
CleanupFn run_effect(void* context) {
Computed* doubled_sig = (Computed*)context;
// Get the value of our dependency
intptr_t val = (intptr_t)computed_get_value(doubled_sig);
printf(">>> (Effect) Value changed: %ld\n", (long)val);
return NULL; // No cleanup needed
}
int main() {
printf("--- Creating signals ---\n");
// 1. Create a base signal with an initial value of 0.
// We cast the integer '0' to void* using intptr_t.
Signal* count = signal_create((void*)(intptr_t)0, "count", NULL, NULL);
// 2. Create a computed signal that doubles the 'count'.
// We pass 'count' as the context for the compute_doubled function.
Computed* doubled = computed_create(compute_doubled, count, "doubled", NULL, NULL);
// 3. Create an effect that prints the value of 'doubled'.
// We pass 'doubled' as the context for the run_effect function.
// The effect runs immediately upon creation.
Effect* print_effect = effect_create(run_effect, doubled, "print_effect");
printf("\n--- Updating signal ---\n");
// 4. Update the base signal. This will trigger the computed,
// which then triggers the effect.
signal_set_value(count, (void*)(intptr_t)5);
printf("\n--- Updating signal again ---\n");
signal_set_value(count, (void*)(intptr_t)10);
printf("\n--- Peeking value (no trigger) ---\n");
// Peeking doesn't register a dependency and won't trigger computes/effects.
intptr_t peek_val = (intptr_t)computed_peek(doubled);
printf("Peeked value: %ld\n", (long)peek_val);
// 5. Check that setting the same value does nothing
printf("\n--- Setting same value (no trigger) ---\n");
signal_set_value(count, (void*)(intptr_t)10);
printf("\n--- Cleaning up ---\n");
// 6. Clean up resources
effect_dispose(print_effect);
computed_free(doubled);
signal_free(count);
return 0;
}
#include "signals.h"
#include <stdlib.h> // for malloc, free, exit
#include <stdio.h> // for fprintf, stderr
#include <string.h> // for strdup
#include <stddef.h> // for offsetof
// --- Static Globals ---
// Currently evaluated computed or effect.
static ReactiveBase* evalContext = NULL;
// Effects collected into a batch.
static Effect* batchedEffect = NULL;
static int batchDepth = 0;
static int batchIteration = 0;
// A global version number for signals.
static int globalVersion = 0;
// --- Static Function Prototypes (Private) ---
static void startBatch(void);
static void endBatch(void);
static Node* addDependency(Signal* signal);
static bool needsToRecompute(ReactiveBase* target);
static void prepareSources(ReactiveBase* target);
static void cleanupSources(ReactiveBase* target);
static void cleanupEffect(Effect* effect);
static void disposeEffect(Effect* effect);
static ReactiveBase* effect_start(Effect* self);
static void effect_end(Effect* self, ReactiveBase* prevContext);
static void effect_callback(Effect* self);
static void effect_dispose_impl(Effect* self);
// Signal "vtable" methods
static bool signal_refresh_impl(Signal* self);
static void signal_subscribe_impl(Signal* self, Node* node);
static void signal_unsubscribe_impl(Signal* self, Node* node);
// Computed "vtable" methods
static bool computed_refresh_impl(Signal* self_sig);
static void computed_subscribe_impl(Signal* self_sig, Node* node);
static void computed_unsubscribe_impl(Signal* self_sig, Node* node);
static void computed_notify_impl(ReactiveBase* self_base);
// Effect "vtable" method
static void effect_notify_impl(ReactiveBase* self_base);
// --- Batching & Untracked Implementation ---
static void startBatch() {
batchDepth++;
}
static void endBatch() {
if (batchDepth > 1) {
batchDepth--;
return;
}
while (batchedEffect != NULL) {
Effect* effect = batchedEffect;
batchedEffect = NULL;
batchIteration++;
while (effect != NULL) {
Effect* next = effect->_nextBatchedEffect;
effect->_nextBatchedEffect = NULL;
effect->reactive._flags &= ~NOTIFIED;
if (!(effect->reactive._flags & DISPOSED) && needsToRecompute(&effect->reactive)) {
// C translation: We cannot try/catch user code.
// If effect_callback() crashes, the whole program crashes.
effect_callback(effect);
}
effect = next;
}
}
batchIteration = 0;
batchDepth--;
// Error handling removed for simplicity.
}
void* batch(void* (*fn)(void* context), void* context) {
if (batchDepth > 0) {
return fn(context);
}
startBatch();
// C translation: No try/finally.
// If fn() crashes, endBatch() is not called, which is a problem.
// This is a limitation of translating JS try/finally to C.
void* result = fn(context);
endBatch();
return result;
}
void* untracked(void* (*fn)(void* context), void* context) {
ReactiveBase* prevContext = evalContext;
evalContext = NULL;
// C translation: No try/finally.
void* result = fn(context);
evalContext = prevContext;
return result;
}
// --- Dependency Tracking Implementation ---
static Node* addDependency(Signal* signal) {
if (evalContext == NULL) {
return NULL;
}
Node* node = signal->_node;
if (node == NULL || node->_target != evalContext) {
// `signal` is a new dependency.
node = (Node*)malloc(sizeof(Node));
if (node == NULL) {
fprintf(stderr, "Out of memory\n");
exit(1);
}
node->_version = 0;
node->_source = signal;
node->_prevSource = evalContext->_sources;
node->_nextSource = NULL;
node->_target = evalContext;
node->_prevTarget = NULL;
node->_nextTarget = NULL;
node->_rollbackNode = signal->_node;
if (evalContext->_sources != NULL) {
evalContext->_sources->_nextSource = node;
}
evalContext->_sources = node;
signal->_node = node;
if (evalContext->_flags & TRACKING) {
signal->_subscribe(signal, node);
}
return node;
} else if (node->_version == -1) {
// `signal` is an existing dependency. Reuse it.
node->_version = 0;
if (node->_nextSource != NULL) {
node->_nextSource->_prevSource = node->_prevSource;
if (node->_prevSource != NULL) {
node->_prevSource->_nextSource = node->_nextSource;
}
node->_prevSource = evalContext->_sources;
node->_nextSource = NULL;
evalContext->_sources->_nextSource = node;
evalContext->_sources = node;
}
return node;
}
return NULL;
}
// --- Signal Implementation ---
static bool signal_refresh_impl(Signal* self) {
(void)self; // Unused
return true;
}
static void signal_subscribe_impl(Signal* self, Node* node) {
Node* targets = self->_targets;
if (targets != node && node->_prevTarget == NULL) {
node->_nextTarget = targets;
self->_targets = node;
if (targets != NULL) {
targets->_prevTarget = node;
} else if (self->_watched != NULL) {
untracked(NULL, NULL); // Hack to call untracked with NULL fn
// Call untracked simple
ReactiveBase* prevContext = evalContext;
evalContext = NULL;
self->_watched(self);
evalContext = prevContext;
}
}
}
static void signal_unsubscribe_impl(Signal* self, Node* node) {
if (self->_targets != NULL) {
Node* prev = node->_prevTarget;
Node* next = node->_nextTarget;
if (prev != NULL) {
prev->_nextTarget = next;
node->_prevTarget = NULL;
}
if (next != NULL) {
next->_prevTarget = prev;
node->_nextTarget = NULL;
}
if (node == self->_targets) {
self->_targets = next;
if (next == NULL && self->_unwatched != NULL) {
// Call untracked simple
ReactiveBase* prevContext = evalContext;
evalContext = NULL;
self->_unwatched(self);
evalContext = prevContext;
}
}
}
}
Signal* signal_create(void* value, const char* name, void (*watched)(Signal*), void (*unwatched)(Signal*)) {
Signal* sig = (Signal*)malloc(sizeof(Signal));
if (sig == NULL) {
fprintf(stderr, "Out of memory\n");
exit(1);
}
sig->_value = value;
sig->_version = 0;
sig->_node = NULL;
sig->_targets = NULL;
sig->name = name ? strdup(name) : NULL;
// Set vtable
sig->_refresh = signal_refresh_impl;
sig->_subscribe = signal_subscribe_impl;
sig->_unsubscribe = signal_unsubscribe_impl;
// Set optional callbacks
sig->_watched = watched;
sig->_unwatched = unwatched;
return sig;
}
void signal_free(Signal* sig) {
if (sig == NULL) return;
// Note: This does not clean up dependencies.
// The user is responsible for ensuring the signal is no longer in use.
if (sig->name) {
free(sig->name);
}
free(sig);
}
void* signal_get_value(Signal* self) {
Node* node = addDependency(self);
if (node != NULL) {
node->_version = self->_version;
}
return self->_value;
}
void signal_set_value(Signal* self, void* value) {
// In C, we can only compare pointers.
// The user is responsible for managing value identity.
if (value != self->_value) {
if (batchIteration > 100) {
fprintf(stderr, "Cycle detected\n");
exit(1);
}
self->_value = value;
self->_version++;
globalVersion++;
startBatch();
for (Node* node = self->_targets; node != NULL; node = node->_nextTarget) {
node->_target->_notify(node->_target);
}
endBatch();
}
}
void* signal_peek(Signal* self) {
ReactiveBase* prevContext = evalContext;
evalContext = NULL;
void* value = self->_value; // No _refresh() or addDependency()
evalContext = prevContext;
return value;
}
// --- Subscription Helper ---
typedef struct {
Signal* signal;
SignalSubscribeFn user_fn;
void* user_context;
} SignalSubscribeContext;
static void free_context_cleanup(void* context) {
free(context);
}
static CleanupFn signal_subscribe_effect_fn(void* context) {
SignalSubscribeContext* ctx = (SignalSubscribeContext*)context;
// Get value (and subscribe)
void* value = signal_get_value(ctx->signal);
// Run user function untracked
ReactiveBase* prevContext = evalContext;
evalContext = NULL;
ctx->user_fn(value, ctx->user_context);
evalContext = prevContext;
// Return a cleanup function that will free our context
return free_context_cleanup;
}
Effect* signal_subscribe(Signal* self, SignalSubscribeFn fn, void* user_context) {
SignalSubscribeContext* ctx = (SignalSubscribeContext*)malloc(sizeof(SignalSubscribeContext));
if (ctx == NULL) {
fprintf(stderr, "Out of memory\n");
exit(1);
}
ctx->signal = self;
ctx->user_fn = fn;
ctx->user_context = user_context;
return effect_create(signal_subscribe_effect_fn, ctx, "sub");
}
// --- Reactive Node Helpers ---
static bool needsToRecompute(ReactiveBase* target) {
for (Node* node = target->_sources; node != NULL; node = node->_nextSource) {
if (node->_source->_version != node->_version ||
!node->_source->_refresh(node->_source) || // Call vtable refresh
node->_source->_version != node->_version)
{
return true;
}
}
return false;
}
static void prepareSources(ReactiveBase* target) {
for (Node* node = target->_sources; node != NULL; node = node->_nextSource) {
Node* rollbackNode = node->_source->_node;
if (rollbackNode != NULL) {
node->_rollbackNode = rollbackNode;
}
node->_source->_node = node;
node->_version = -1;
if (node->_nextSource == NULL) {
target->_sources = node;
break;
}
}
}
static void cleanupSources(ReactiveBase* target) {
Node* node = target->_sources;
Node* head = NULL;
while (node != NULL) {
Node* prev = node->_prevSource;
// Restore the previous _node pointer on the source signal.
node->_source->_node = node->_rollbackNode;
if (node->_version == -1) {
// Unused node. Unsubscribe and free it.
node->_source->_unsubscribe(node->_source, node);
if (prev != NULL) {
prev->_nextSource = node->_nextSource;
}
if (node->_nextSource != NULL) {
node->_nextSource->_prevSource = prev;
}
free(node); // Free the unused node
} else {
// This node was used. It becomes the new head.
head = node;
}
node = prev;
}
target->_sources = head;
}
// --- Computed Implementation ---
static bool computed_refresh_impl(Signal* self_sig) {
Computed* self = (Computed*)self_sig; // Cast from base Signal
self->reactive._flags &= ~NOTIFIED;
if (self->reactive._flags & RUNNING) {
return false; // Cycle
}
if ((self->reactive._flags & (OUTDATED | TRACKING)) == TRACKING) {
return true; // Not outdated
}
self->reactive._flags &= ~OUTDATED;
if (self->_globalVersion == globalVersion) {
return true; // No global changes
}
self->_globalVersion = globalVersion;
self->reactive._flags |= RUNNING;
if (self->base._version > 0 && !needsToRecompute(&self->reactive)) {
self->reactive._flags &= ~RUNNING;
return true;
}
ReactiveBase* prevContext = evalContext;
prepareSources(&self->reactive);
evalContext = &self->reactive;
// No try/catch in C
void* value = self->_fn(self->_context);
if (self->base._value != value || self->base._version == 0) {
self->base._value = value;
self->base._version++;
}
evalContext = prevContext;
cleanupSources(&self->reactive);
self->reactive._flags &= ~RUNNING;
return true;
}
static void computed_subscribe_impl(Signal* self_sig, Node* node) {
Computed* self = (Computed*)self_sig;
if (self->base._targets == NULL) {
self->reactive._flags |= OUTDATED | TRACKING;
for (Node* n = self->reactive._sources; n != NULL; n = n->_nextSource) {
n->_source->_subscribe(n->_source, n);
}
}
signal_subscribe_impl(self_sig, node); // Call "base" method
}
static void computed_unsubscribe_impl(Signal* self_sig, Node* node) {
Computed* self = (Computed*)self_sig;
if (self->base._targets != NULL) {
signal_unsubscribe_impl(self_sig, node); // Call "base" method
if (self->base._targets == NULL) {
self->reactive._flags &= ~TRACKING;
for (Node* n = self->reactive._sources; n != NULL; n = n->_nextSource) {
n->_source->_unsubscribe(n->_source, n);
}
}
}
}
static void computed_notify_impl(ReactiveBase* self_base) {
// Get container Computed* from ReactiveBase* member
Computed* self = (Computed*)((char*)self_base - offsetof(Computed, reactive));
if (!(self->reactive._flags & NOTIFIED)) {
self->reactive._flags |= OUTDATED | NOTIFIED;
for (Node* node = self->base._targets; node != NULL; node = node->_nextTarget) {
node->_target->_notify(node->_target);
}
}
}
Computed* computed_create(ComputedFn fn, void* context, const char* name, void (*watched)(Signal*), void (*unwatched)(Signal*)) {
Computed* comp = (Computed*)malloc(sizeof(Computed));
if (comp == NULL) {
fprintf(stderr, "Out of memory\n");
exit(1);
}
// Initialize Signal base
comp->base._value = NULL;
comp->base._version = 0;
comp->base._node = NULL;
comp->base._targets = NULL;
comp->base.name = name ? strdup(name) : NULL;
comp->base._watched = watched;
comp->base._unwatched = unwatched;
// Set Signal vtable
comp->base._refresh = computed_refresh_impl;
comp->base._subscribe = computed_subscribe_impl;
comp->base._unsubscribe = computed_unsubscribe_impl;
// Initialize ReactiveBase
comp->reactive._sources = NULL;
comp->reactive._flags = OUTDATED;
comp->reactive._notify = computed_notify_impl; // Set notify vtable
// Initialize Computed fields
comp->_fn = fn;
comp->_context = context;
comp->_globalVersion = globalVersion - 1;
return comp;
}
void computed_free(Computed* comp) {
if (comp == NULL) return;
// Free all source nodes
Node* node = comp->reactive._sources;
while (node != NULL) {
Node* next = node->_nextSource;
// Unsubscribe from the source's targets list
node->_source->_unsubscribe(node->_source, node);
free(node);
node = next;
}
if (comp->base.name) {
free(comp->base.name);
}
free(comp);
}
void* computed_get_value(Computed* self) {
if (self->reactive._flags & RUNNING) {
fprintf(stderr, "Cycle detected\n");
exit(1);
}
Node* node = addDependency(&self->base);
self->base._refresh(&self->base); // Call vtable refresh
if (node != NULL) {
node->_version = self->base._version;
}
return self->base._value;
}
void* computed_peek(Computed* self) {
ReactiveBase* prevContext = evalContext;
evalContext = NULL;
// Call refresh to ensure the value is up to date, but don't track
self->base._refresh(&self->base);
void* value = self->base._value;
evalContext = prevContext;
return value;
}
// --- Effect Implementation ---
static void cleanupEffect(Effect* effect) {
CleanupFn cleanup = effect->_cleanup;
effect->_cleanup = NULL;
if (cleanup != NULL) {
startBatch();
ReactiveBase* prevContext = evalContext;
evalContext = NULL;
cleanup(effect->_context); // Pass back the original context
evalContext = prevContext;
endBatch();
}
}
static void disposeEffect(Effect* effect) {
Node* node = effect->reactive._sources;
while (node != NULL) {
Node* next = node->_nextSource;
node->_source->_unsubscribe(node->_source, node);
free(node); // Free the node
node = next;
}
effect->reactive._sources = NULL;
effect->_fn = NULL; // Mark as disposed
cleanupEffect(effect);
}
static ReactiveBase* effect_start(Effect* self) {
if (self->reactive._flags & RUNNING) {
fprintf(stderr, "Cycle detected\n");
exit(1);
}
self->reactive._flags |= RUNNING;
self->reactive._flags &= ~DISPOSED;
cleanupEffect(self);
prepareSources(&self->reactive);
startBatch();
ReactiveBase* prevContext = evalContext;
evalContext = &self->reactive;
return prevContext;
}
static void effect_end(Effect* self, ReactiveBase* prevContext) {
if (evalContext != &self->reactive) {
fprintf(stderr, "Out-of-order effect\n");
exit(1);
}
cleanupSources(&self->reactive);
evalContext = prevContext;
self->reactive._flags &= ~RUNNING;
if (self->reactive._flags & DISPOSED) {
disposeEffect(self);
}
endBatch();
}
static void effect_callback(Effect* self) {
ReactiveBase* prevContext = effect_start(self);
if (self->reactive._flags & DISPOSED) {
effect_end(self, prevContext);
return;
}
if (self->_fn == NULL) {
effect_end(self, prevContext);
return;
}
CleanupFn cleanup = self->_fn(self->_context);
if (cleanup != NULL) {
self->_cleanup = cleanup;
}
effect_end(self, prevContext);
}
static void effect_notify_impl(ReactiveBase* self_base) {
// Get container Effect* from ReactiveBase* member
Effect* self = (Effect*)((char*)self_base - offsetof(Effect, reactive));
if (!(self->reactive._flags & NOTIFIED)) {
self->reactive._flags |= NOTIFIED;
self->_nextBatchedEffect = batchedEffect;
batchedEffect = self;
}
}
static void effect_dispose_impl(Effect* self) {
self->reactive._flags |= DISPOSED;
if (!(self->reactive._flags & RUNNING)) {
disposeEffect(self);
}
}
Effect* effect_create(EffectFn fn, void* context, const char* name) {
Effect* effect = (Effect*)malloc(sizeof(Effect));
if (effect == NULL) {
fprintf(stderr, "Out of memory\n");
exit(1);
}
// Initialize ReactiveBase
effect->reactive._sources = NULL;
effect->reactive._flags = TRACKING;
effect->reactive._notify = effect_notify_impl; // Set notify vtable
// Initialize Effect fields
effect->_fn = fn;
effect->_context = context;
effect->_cleanup = NULL;
effect->_nextBatchedEffect = NULL;
effect->name = name ? strdup(name) : NULL;
// Run the effect immediately
// No try/catch in C.
effect_callback(effect);
return effect;
}
void effect_dispose(Effect* effect) {
if (effect == NULL) return;
effect_dispose_impl(effect);
// Once disposed and not running, we can free the memory
if (effect->name) {
free(effect->name);
}
free(effect);
}
#ifndef SIGNALS_H
#define SIGNALS_H
#include <stddef.h>
#include <stdbool.h>
// --- Forward Declarations ---
struct Signal;
struct Computed;
struct Effect;
struct Node;
// --- Flags ---
// Flags for Computed and Effect.
typedef enum {
RUNNING = 1 << 0,
NOTIFIED = 1 << 1,
OUTDATED = 1 << 2,
DISPOSED = 1 << 3,
// HAS_ERROR (removed for C, error handling is simplified)
TRACKING = 1 << 5
} SignalFlags;
// --- Callback Function Pointer Types ---
/**
* Function pointer for a computed signal's calculation.
* @param context The user-provided context.
* @return A void* pointer to the computed value.
*/
typedef void* (*ComputedFn)(void* context);
/**
* Function pointer for an effect's cleanup logic.
* @param context The user-provided context that was passed to the effect.
*/
typedef void (*CleanupFn)(void* context);
/**
* Function pointer for an effect's execution.
* @param context The user-provided context.
* @return An optional CleanupFn to be run before the next execution or on dispose.
*/
typedef CleanupFn (*EffectFn)(void* context);
/**
* Function pointer for a signal subscription callback.
* @param value The new void* pointer value from the signal.
* @param context The user-provided context.
*/
typedef void (*SignalSubscribeFn)(void* value, void* context);
// --- Core Structs ---
/**
* @internal
* Base struct for reactive components (Computed and Effect).
* Ensures _sources and _flags are at the same offset.
*/
typedef struct ReactiveBase {
struct Node* _sources;
int _flags;
// vtable-like pointer for _notify
void (*_notify)(struct ReactiveBase* self);
} ReactiveBase;
/**
* @internal
* A linked list node used to track dependencies (sources) and dependents (targets).
*/
typedef struct Node {
struct Signal* _source;
struct Node* _prevSource;
struct Node* _nextSource;
ReactiveBase* _target;
struct Node* _prevTarget;
struct Node* _nextTarget;
int _version;
struct Node* _rollbackNode;
} Node;
/**
* The base struct for plain and computed signals.
*/
typedef struct Signal {
void* _value;
int _version;
struct Node* _node;
struct Node* _targets;
// vtable for polymorphic methods
bool (*_refresh)(struct Signal* self);
void (*_subscribe)(struct Signal* self, struct Node* node);
void (*_unsubscribe)(struct Signal* self, struct Node* node);
// Optional user-provided callbacks
void (*_watched)(struct Signal* self);
void (*_unwatched)(struct Signal* self);
char* name;
} Signal;
/**
* The struct for computed signals.
* The first member *must* be the Signal base for type-punning.
*/
typedef struct Computed {
Signal base; // "Inherits" from Signal
ReactiveBase reactive; // "Implements" ReactiveBase
ComputedFn _fn;
void* _context; // User context for _fn
int _globalVersion;
} Computed;
/**
* The struct for effects.
* The first member *must* be the ReactiveBase for type-punning.
*/
typedef struct Effect {
ReactiveBase reactive; // "Implements" ReactiveBase
EffectFn _fn;
void* _context; // User context for _fn
CleanupFn _cleanup;
struct Effect* _nextBatchedEffect;
char* name;
} Effect;
// --- Public API ---
/**
* Creates a new plain signal.
* @param value The initial value (as a void*).
* @param name Optional name for debugging.
* @param watched Optional callback when the first subscriber is added.
* @param unwatched Optional callback when the last subscriber is removed.
* @return A pointer to the new Signal. Must be freed with signal_free().
*/
Signal* signal_create(void* value, const char* name, void (*watched)(Signal*), void (*unwatched)(Signal*));
/**
* Frees the memory for a plain signal.
* @warning The caller is responsible for ensuring no other part of the system
* (like a Computed or Effect) is still using this signal.
*/
void signal_free(Signal* sig);
/**
* Gets the current value of the signal and registers a dependency if in a context.
* @param self The Signal.
* @return The current value (as a void*).
*/
void* signal_get_value(Signal* self);
/**
* Sets the value of the signal, incrementing the version and notifying dependents.
* @param self The Signal.
* @param value The new value (as a void*).
*/
void signal_set_value(Signal* self, void* value);
/**
* Gets the current value of the signal without registering a dependency.
* @param self The Signal.
* @return The current value (as a void*).
*/
void* signal_peek(Signal* self);
/**
* Subscribes to changes in a signal's value.
* This is a helper function that creates an effect.
* @param self The Signal to subscribe to.
* @param fn The callback function to run on change.
* @param context User context to pass to the callback.
* @return An Effect pointer. The caller must call effect_dispose() to stop
* the subscription and free the memory.
*/
Effect* signal_subscribe(Signal* self, SignalSubscribeFn fn, void* user_context);
/**
* Creates a new computed signal.
* @param fn The function to compute the value.
* @param context User context to pass to the compute function.
* @param name Optional name for debugging.
* @param watched Optional callback.
* @param unwatched Optional callback.
* @return A pointer to the new Computed signal. Must be freed with computed_free().
*/
Computed* computed_create(ComputedFn fn, void* context, const char* name, void (*watched)(Signal*), void (*unwatched)(Signal*));
/**
* Frees the memory for a computed signal, including its dependencies.
*/
void computed_free(Computed* comp);
/**
* Gets the current value of the computed signal.
* This will re-run the computation if dependencies have changed.
* @param self The Computed signal.
* @return The current value (as a void*).
*/
void* computed_get_value(Computed* self);
/**
* Gets the current value of the computed signal without registering a dependency.
* @param self The Computed signal.
* @return The current value (as a void*).
*/
void* computed_peek(Computed* self);
/**
* Creates a new effect.
* The effect runs immediately and then re-runs whenever its dependencies change.
* @param fn The effect callback function.
* @param context User context to pass to the effect function.
* @param name Optional name for debugging.
* @return A pointer to the new Effect. Must be disposed with effect_dispose().
*/
Effect* effect_create(EffectFn fn, void* context, const char* name);
/**
* Disposes of an effect, running its cleanup function and freeing its memory.
* @param effect The Effect to dispose.
*/
void effect_dispose(Effect* effect);
/**
* Combine multiple value updates into one "commit".
* @param fn The callback function to run.
* @param context User context to pass to the callback.
* @return The void* value returned by the callback.
*/
void* batch(void* (*fn)(void* context), void* context);
/**
* Run a callback function without subscribing to any signal updates.
* @param fn The callback function to run.
* @param context User context to pass to the callback.
* @return The void* value returned by the callback.
*/
void* untracked(void* (*fn)(void* context), void* context);
#endif // SIGNALS_H
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment