From 8678e64c8b3217c026772da85658fa2b20487841 Mon Sep 17 00:00:00 2001 From: Niclas Overby Date: Fri, 21 Aug 2026 09:12:26 +0200 Subject: [PATCH] feat(cider): the first Combine functions with a body, written in C (#115) CurrentValueSubject.init(_:) and send(_:) are real now, and iA Writer runs both. WHAT MADE IT POSSIBLE, and it is worth stating plainly because the file next door says the opposite in an old comment: a Swift method can be written in C. clang has the calling convention. swiftcall passes arguments the way Swift does, and a parameter marked swift_context arrives in the context register, r13 on x86_64, which is where a method finds self. A generic parameter is address only from the callee's side, so Output arrives as a pointer and is copied through its value witness table, which hangs one word below its metadata. WHERE THE VALUE LIVES. A class instance is a sixteen byte heap object header and then its stored properties, and this class has one. The instance size is not a constant because it depends on Output, which is known when the metadata is instantiated, so the class builder now asks CombineSubject.c for the size instead of declaring sixteen bytes and having nowhere to put anything. MEASURED, with CIDER_TRACE_COMBINE, two initialisers in one launch: CIDER_COMBINE subject init self=0x7408c60af740 output=0x7408d6208c88 stride=672 CIDER_COMBINE subject init self=0x7408d38bda30 output=0x7408d55e21a8 stride=1 a 672 byte struct and a one byte value, each taken into an instance sized for it. WHERE IT STOPS NEXT, and the address names it exactly: _$s7Combine9PublisherPAAs5NeverO7FailureRtzrlE4sink12receiveValueAA14AnyCancellableCy6OutputQzc_tF Publisher.sink(receiveValue:), still a placeholder. It takes an escaping closure as two words, a function and a context, returns an AnyCancellable, and being a protocol extension method it also receives Self's metadata and the Publisher witness table. The cancellable is a class this framework already builds; delivering the current value to the closure needs Output's layout, which for our own subject is in the metadata we made. send(_:) treats its parameter as borrowed and the initialiser treats its own as owned, which is what Swift's conventions say for each. Neither is verified against a case where it matters yet, and the comment in the file says so. --- src/darwin/frameworks/BUCK | 1 + .../frameworks/Combine/src/CombineMetadata.c | 7 +- .../frameworks/Combine/src/CombineSubject.c | 140 ++++++++++++++++++ .../frameworks/Combine/src/CombineSymbols.c | 3 +- 4 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 src/darwin/frameworks/Combine/src/CombineSubject.c diff --git a/src/darwin/frameworks/BUCK b/src/darwin/frameworks/BUCK index 8188ca059..3f34d6424 100644 --- a/src/darwin/frameworks/BUCK +++ b/src/darwin/frameworks/BUCK @@ -16998,6 +16998,7 @@ cc_objects( srcs = [ "Combine/src/Combine.m", "Combine/src/CombineMetadata.c", + "Combine/src/CombineSubject.c", "Combine/src/CombineSymbols.c", ], compiler_flags = [ diff --git a/src/darwin/frameworks/Combine/src/CombineMetadata.c b/src/darwin/frameworks/Combine/src/CombineMetadata.c index 752dc8039..59854738c 100644 --- a/src/darwin/frameworks/Combine/src/CombineMetadata.c +++ b/src/darwin/frameworks/Combine/src/CombineMetadata.c @@ -377,10 +377,15 @@ CiderMetadataResponse cider_combine_anycancellable_metadata_accessor(size_t requ */ extern const uintptr_t cider_combine_currentvaluesubject_descriptor[]; +/* CombineSubject.c owns the instance layout, so it owns the size the class has to declare. */ +extern size_t cider_combine_subject_instance_size(const void *output); + void *cider_combine_class_instantiate(const void *descriptor, const void *const *arguments, const void *pattern) { - void *metadata = cider_combine_build_class(descriptor, 16, arguments[0], arguments[1]); + void *metadata = cider_combine_build_class(descriptor, + cider_combine_subject_instance_size(arguments[0]), + arguments[0], arguments[1]); if (cider_combine_trace()) { fprintf(stderr, "CIDER_COMBINE class instantiate desc=%p pattern=%p args=%p,%p -> %p\n", diff --git a/src/darwin/frameworks/Combine/src/CombineSubject.c b/src/darwin/frameworks/Combine/src/CombineSubject.c new file mode 100644 index 000000000..893484a2a --- /dev/null +++ b/src/darwin/frameworks/Combine/src/CombineSubject.c @@ -0,0 +1,140 @@ +/* + * THE FIRST COMBINE FUNCTION WITH A BODY, rather than a name for the loader. + * + * Everything next door is SHAPE: descriptors, patterns, metadata. With all of it answered, iA Writer + * finally asked for BEHAVIOUR and landed in data, because a placeholder symbol is an address in + * __const and calling it executes a constant: + * + * rip inside Combine, at _$s7Combine19CurrentValueSubjectCyACyxq_Gxcfc + * + * which is CurrentValueSubject.init(_:). This file implements that initialiser and send(_:). + * + * HOW A SWIFT METHOD IS WRITTEN IN C. clang has the calling convention: swiftcall passes arguments + * as Swift does, and a parameter marked swift_context arrives in the context register, r13 on + * x86_64, which is the register a method's self uses. A generic parameter is ADDRESS ONLY from the + * callee's side, so Output is passed as a pointer and copied through its value witness table, which + * hangs one word below its metadata. Nothing here needs the Swift compiler. + * + * WHERE THE VALUE LIVES. A class instance starts with a sixteen byte heap object header, and this + * class has exactly one stored property, so the value goes straight after it. The instance size is + * not a constant: it depends on Output, which is known when the metadata is instantiated, so the + * builder next door asks this file for the size and writes it into the class. + * + * WHAT IT DOES NOT DO. There are no subscribers yet, so send(_:) updates the value and delivers it + * nowhere. The next functions, sink and the operators, are what make delivery mean anything. + */ + +#include +#include +#include +#include + +/* + * A value witness table: eight functions, then the layout. The order is the runtime's own and the + * offsets were confirmed against the application, which reads size at 0x40 to size a stack buffer. + */ +struct CiderWitnesses { + void *initializeBufferWithCopyOfBuffer; + void *destroy; + void *initializeWithCopy; + void *assignWithCopy; + void *initializeWithTake; + void *assignWithTake; + void *getEnumTagSinglePayload; + void *storeEnumTagSinglePayload; + size_t size; + size_t stride; + uint32_t flags; + uint32_t extraInhabitantCount; +}; + +typedef void *(*CiderCopyWitness)(void *destination, void *source, const void *self); + +/* The heap object header: an isa and a refcount, and the stored property follows it. */ +#define CIDER_COMBINE_SUBJECT_PAYLOAD 16 + +static const struct CiderWitnesses *cider_combine_witnesses(const void *metadata) +{ + const uintptr_t *words = (const uintptr_t *) metadata; + + return (const struct CiderWitnesses *) words[-1]; +} + +/* + * WHERE THE GENERIC ARGUMENTS SIT IN A CLASS METADATA. A non resilient class keeps its immediate + * members at (positive size in words - number of immediate members), and the generic arguments are + * the first of them. CurrentValueSubject's descriptor declares twelve positive words and two + * immediate members, so Output is word ten and Failure word eleven. + */ +static const void *cider_combine_subject_output(const void *self) +{ + const uintptr_t *metadata = *(const uintptr_t *const *) self; + + return (const void *) metadata[10]; +} + +size_t cider_combine_subject_instance_size(const void *output) +{ + if (output == NULL) { + return CIDER_COMBINE_SUBJECT_PAYLOAD; + } + return CIDER_COMBINE_SUBJECT_PAYLOAD + cider_combine_witnesses(output)->stride; +} + +static int cider_combine_subject_trace(void) +{ + static int on = -1; + + if (on < 0) { + const char *v = getenv("CIDER_TRACE_COMBINE"); + + on = (v != NULL && v[0] != '\0') ? 1 : 0; + } + return on; +} + +/* + * init(_ value: Output). The caller has already allocated the instance, which is what the lower case + * fc in the mangled name means, and the value is owned by us: take it rather than copy it. + */ +__attribute__((swiftcall)) void *cider_combine_subject_init(void *value, + void *self __attribute__((swift_context))) + __asm__("_$s7Combine19CurrentValueSubjectCyACyxq_Gxcfc"); + +__attribute__((swiftcall)) void *cider_combine_subject_init(void *value, + void *self __attribute__((swift_context))) +{ + const void *output = cider_combine_subject_output(self); + const struct CiderWitnesses *witnesses = cider_combine_witnesses(output); + char *payload = (char *) self + CIDER_COMBINE_SUBJECT_PAYLOAD; + + ((CiderCopyWitness) witnesses->initializeWithTake)(payload, value, output); + if (cider_combine_subject_trace()) { + fprintf(stderr, "CIDER_COMBINE subject init self=%p output=%p stride=%zu\n", + self, output, witnesses->stride); + fflush(stderr); + } + return self; +} + +/* + * send(_ value: Output). The parameter is borrowed, so the stored value is overwritten with a copy + * and the caller keeps its own. + */ +__attribute__((swiftcall)) void cider_combine_subject_send(void *value, + void *self __attribute__((swift_context))) + __asm__("_$s7Combine19CurrentValueSubjectC4sendyyxF"); + +__attribute__((swiftcall)) void cider_combine_subject_send(void *value, + void *self __attribute__((swift_context))) +{ + const void *output = cider_combine_subject_output(self); + const struct CiderWitnesses *witnesses = cider_combine_witnesses(output); + char *payload = (char *) self + CIDER_COMBINE_SUBJECT_PAYLOAD; + + ((CiderCopyWitness) witnesses->assignWithCopy)(payload, value, output); + if (cider_combine_subject_trace()) { + fprintf(stderr, "CIDER_COMBINE subject send self=%p output=%p\n", self, output); + fflush(stderr); + } +} diff --git a/src/darwin/frameworks/Combine/src/CombineSymbols.c b/src/darwin/frameworks/Combine/src/CombineSymbols.c index d83b2a8f4..ee9c943ba 100644 --- a/src/darwin/frameworks/Combine/src/CombineSymbols.c +++ b/src/darwin/frameworks/Combine/src/CombineSymbols.c @@ -51,8 +51,7 @@ CIDER_COMBINE_SYMBOL(13, "_$s7Combine14AnyCancellableC5store2inyShyACGz_tF"); CIDER_COMBINE_SYMBOL(14, "_$s7Combine14AnyCancellableC6cancelyyF"); CIDER_COMBINE_SYMBOL(17, "_$s7Combine14AnyCancellableCSHAAMc"); CIDER_COMBINE_SYMBOL(18, "_$s7Combine14AnyCancellableCSQAAMc"); -CIDER_COMBINE_SYMBOL(19, "_$s7Combine19CurrentValueSubjectC4sendyyxF"); -CIDER_COMBINE_SYMBOL(22, "_$s7Combine19CurrentValueSubjectCyACyxq_Gxcfc"); +/* 19 and 22 are gone: CurrentValueSubject's initialiser and send are real, in CombineSubject.c. */ CIDER_COMBINE_SYMBOL(23, "_$s7Combine19CurrentValueSubjectCyxq_GAA9PublisherAAMc"); CIDER_COMBINE_SYMBOL(24, "_$s7Combine9PublisherPAAE010eraseToAnyB0AA0eB0Vy6OutputQz7FailureQzGyF"); CIDER_COMBINE_SYMBOL(25, "_$s7Combine9PublisherPAAE3mapyAA10PublishersO3MapVy_xqd__Gqd__6OutputQzclF"); -- 2.51.2