Last active
November 14, 2025 01:30
-
-
Save rodydavis/d76860eaa9ffba008278ccbf995f1db0 to your computer and use it in GitHub Desktop.
Preact Signals in C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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