git » sdk » commit e65f0a8

First pass at exposing interfaces to C

author Stephen Paul Weber
2026-01-14 21:18:24 UTC
committer Stephen Paul Weber
2026-01-14 21:18:24 UTC
parent 8641b75288ccacf2df7d3a067a3111c1dc2816ae

First pass at exposing interfaces to C

This creates a companion object full of static methods which is pretty
similar in most cases to the one Haxe already generates internally, but
when something needs to be wrapped then our version uses the wrapper.
Probably we could dedupe this a bit but it works for now.

The generated companion object is also in a new module so it can't use
the imports of the parent, which means sometimes you need fully
qualified names in the interface or the companion object won't generate
a working thing.

HaxeCBridge.hx +67 -16
HaxeSwiftBridge.hx +207 -176
borogove/Form.hx +1 -0
borogove/Persistence.hx +14 -1
borogove/calls/Session.hx +11 -1
borogove/persistence/KeyValueStore.hx +5 -0
borogove/persistence/MediaStore.hx +5 -0

diff --git a/HaxeCBridge.hx b/HaxeCBridge.hx
index 65359f5..e25104a 100644
--- a/HaxeCBridge.hx
+++ b/HaxeCBridge.hx
@@ -96,7 +96,7 @@ class HaxeCBridge {
 	static public function expose(?namespace: String) {
 		var clsRef = Context.getLocalClass(); 
 		var cls = clsRef.get();
-		var fields = Context.getBuildFields();
+		var buildFields = Context.getBuildFields();
 
 		if (libName == null) {
 			// if we cannot determine a libName from --main or -D, we use the first exposed class
@@ -107,10 +107,27 @@ class HaxeCBridge {
 			}
 		}
 
-		queuedClasses.push({
-			cls: clsRef,
-			namespace: namespace
-		});
+		final companionFields = [];
+		final companion: TypeDefinition = {
+			pack: cls.pack,
+			name: cls.name + "__Companion",
+			pos: cls.pos,
+			kind: TDClass(null, [], false, true, false),
+			meta: [
+				{ pos: cls.pos, name: ":keep" },
+				{ pos: cls.pos, name: "HaxeCBridge.name", params: [macro $v{cls.pack.join("_") + "_" + cls.name}]}
+			],
+			fields: companionFields,
+		};
+		final fields = if (cls.isInterface) {
+			companionFields;
+		} else {
+			queuedClasses.push({
+				cls: clsRef,
+				namespace: namespace
+			});
+			buildFields;
+		}
 
 		// add @:keep
 		cls.meta.add(':keep', [], Context.currentPos());
@@ -175,7 +192,7 @@ class HaxeCBridge {
 			firstRun = false;
 		}
 
-		final forloop = fields.slice(0);
+		final forloop = buildFields.slice(0);
 		var insertTo = 0;
 		for (field in forloop) {
 			insertTo++;
@@ -306,14 +323,22 @@ class HaxeCBridge {
 						if (wrapper.doc != null) wrapper.doc = ~/@returns Promise resolving to/.replace(wrapper.doc, "@param handler which receives");
 					default:
 					}
-					if (wrap) {
-						if (outPtr) {
-							wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: macro { final out = $i{field.name}($a{passArgs}); if (outPtr != null) { cpp.Pointer.fromRaw(outPtr).set_ref(out); } return out.length; }, args: args});
+					if (wrap || cls.isInterface) {
+						final pth = if (cls.isInterface) {
+							wrapper.access.push(AStatic);
+							args.insert(0, { name: "self", type: TPath({ pack: cls.pack, name: cls.name }) });
+							["self", field.name];
+						} else {
+							[field.name];
+						}
+						final expr = if (outPtr) {
+							macro { final out = $p{pth}($a{passArgs}); if (outPtr != null) { cpp.Pointer.fromRaw(outPtr).set_ref(out); } return out.length; };
 						} else if (promisify.length > 0) {
-							wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: macro if (handler == null) $i{field.name}($a{passArgs}); else $i{field.name}($a{passArgs}).then(v->handler($a{promisify}), e->handler($a{promisifyE})), args: args});
+							macro if (handler == null) $p{pth}($a{passArgs}); else $p{pth}($a{passArgs}).then(v->handler($a{promisify}), e->handler($a{promisifyE}));
 						} else {
-							wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: macro return $i{field.name}($a{passArgs}), args: args});
+							macro return $p{pth}($a{passArgs});
 						}
+						wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: expr, args: args});
 						fields.insert(insertTo, wrapper);
 						insertTo++;
 						field.meta.push({name: "HaxeCBridge.noemit", params: [{ pos: field.pos, expr: EConst(CString("wrapped")) }], pos: field.pos});
@@ -361,14 +386,16 @@ class HaxeCBridge {
 						});
 						insertTo++;
 					default:
+						final selfArg = cls.isInterface ? [{ name: "__self", type: TPath({ pack: cls.pack, name: cls.name }) }] : [];
+						final pth = cls.isInterface ? ["__self", field.name] : [field.name];
 						if (get != "null" && get != "never") {
 							fields.insert(insertTo, {
 								name: field.name + "__fromC",
 								doc: field.doc,
 								meta: [{name: "HaxeCBridge.wrapper", params: [], pos: field.pos}],
-								access: field.access,
+								access: field.access.concat(cls.isInterface ? [AStatic] : []),
 								pos: field.pos,
-								kind: FFun({ret: t, params: [], args: [], expr: macro { return $i{field.name} }})
+								kind: FFun({ret: t, params: [], args: selfArg, expr: macro { return $p{pth} }})
 							});
 							insertTo++;
 						}
@@ -379,7 +406,7 @@ class HaxeCBridge {
 								meta: [{name: "HaxeCBridge.wrapper", params: [], pos: field.pos}],
 								access: field.access,
 								pos: field.pos,
-								kind: FFun({ret: TPath({name: "Void", pack: []}), params: [], args: [{name: "value", type: t}], expr: macro $i{field.name} = value})
+								kind: FFun({ret: TPath({name: "Void", pack: []}), params: [], args: selfArg.concat([{name: "value", type: t}]), expr: macro $p{pth} = value})
 							});
 							insertTo++;
 						}
@@ -441,7 +468,31 @@ class HaxeCBridge {
 			}
 		}
 
-		return fields;
+		if (cls.isInterface) {
+			// An interface can't contain methods so we make a companion object
+			// Haxe already makes one in C++... but this lets us customize it
+			// and also works with the rest of the codegen without more changes
+			final fullPath = companion.pack.join(".") + "." + companion.name;
+			Context.defineModule(
+				fullPath,
+				[companion],
+				[
+					{ path: [{ pos: companion.pos, name: "HaxeCBridge" }], mode: INormal },
+					{ path: cls.module.split(".").map(p -> { pos: companion.pos, name: p }), mode: INormal }
+				]
+			);
+			final ct = Context.getType(fullPath);
+			switch (ct) {
+				case TInst(ref, _):
+					queuedClasses.push({
+						cls: ref,
+						namespace: namespace
+					});
+				default: Context.error("Comanion must be a class", companion.pos);
+			}
+		}
+
+		return buildFields;
 	}
 
 	static function convertSecondaryTP(tp: TypeParam) {
@@ -530,7 +581,7 @@ class HaxeCBridge {
 		function convertFunction(f: ClassField, kind: FunctionInfoKind) {
 			var isConvertibleMethod = f.isPublic && !f.isAbstract && !f.isExtern && !f.meta.has("HaxeCBridge.noemit") && switch f.kind {
 				case FVar(_), FMethod(MethMacro): false; // skip macro methods
-				case FMethod(_): true;
+				case FMethod(_): f.expr() != null;
 			}
 
 			if (!isConvertibleMethod) return;
diff --git a/HaxeSwiftBridge.hx b/HaxeSwiftBridge.hx
index 6b225c8..e09750a 100644
--- a/HaxeSwiftBridge.hx
+++ b/HaxeSwiftBridge.hx
@@ -288,6 +288,200 @@ class HaxeSwiftBridge {
 		}
 	}
 
+	static function buildVar(f: ClassField, libName: String, cFuncNameGet: String, cFuncNameSet: String, read: VarAccess, write: VarAccess, isStatic: Bool, builder: hx.strings.StringBuilder, genBody: Bool, genAccess: Bool) {
+		builder.add("\t");
+		if (genAccess) builder.add("public ");
+		builder.add("var ");
+		builder.add(f.name);
+		builder.add(": ");
+		builder.add(getSwiftType(f.type));
+		builder.add(" {\n");
+		if (read == AccNormal || read == AccCall) {
+			builder.add("\t\tget");
+			if (genBody) {
+				builder.add(" {\n\t\t\t");
+				builder.add(castToSwift('c_${libName}.${cFuncNameGet}(${isStatic ? '' : 'o'})', f.type, false, true));
+				builder.add("\n\t\t}");
+			}
+			builder.add("\n");
+		}
+		if (write == AccNormal || write == AccCall) {
+			builder.add("\t\tset");
+			if (genBody) {
+				builder.add(" {\n\t\t\tc_");
+				builder.add(libName);
+				builder.add(".");
+				builder.add(cFuncNameSet);
+				builder.add("(" + (isStatic ? "" : "o, "));
+				builder.add(castToC("newValue", f.type));
+				switch TypeTools.followWithAbstracts(Context.resolveType(Context.toComplexType(f.type), Context.currentPos()), false) {
+				case TInst(_.get().name => "Array", [param]):
+					builder.add(", ");
+					builder.add("newValue.count");
+				default:
+				}
+				builder.add(")\n\t\t}");
+			}
+			builder.add("\n");
+		}
+		builder.add("\t}\n\n");
+	}
+
+	static function mkSwiftAsync(ibuilder: hx.strings.StringBuilder, ret: haxe.macro.Type) {
+		ibuilder.add("{ (");
+		ibuilder.add("a");
+		switch (ret) {
+		case TInst(_.get().name => "Array", params):
+			ibuilder.add(", a_length");
+		default:
+		}
+		ibuilder.add(", ctx");
+		ibuilder.add(") in\n\t\t\t\tlet cont = Unmanaged<AnyObject>.fromOpaque(ctx!).takeRetainedValue() as! UnsafeContinuation<");
+		ibuilder.add(getSwiftType(ret));
+		ibuilder.add(", Never>\n\t\t\t\t");
+		final cbuilder = new hx.strings.StringBuilder("cont.resume");
+		cbuilder.add("(returning: ");
+		cbuilder.add(castToSwift("a", ret));
+		cbuilder.add(")");
+		ibuilder.add(cbuilder.toString());
+		ibuilder.add("\n\t\t\t},\n\t\t\t__");
+		ibuilder.add("cont_ptr");
+	}
+
+	static function buildMember(funcName: String, cFuncName: String, fld: Field, targs: Array<{ t: Type, opt: Bool, name: String }>, tret: Type, builder: hx.strings.StringBuilder, genBody: Bool, genAccess: Bool) {
+		var finalTret = tret;
+		builder.add("\t");
+		if (genAccess) builder.add("public ");
+		builder.add("func ");
+		builder.add(funcName);
+		builder.add("(");
+		convertArgs(builder, targs, fld?.kind);
+		builder.add(") ");
+		switch tret {
+			case TAbstract(_.get().name => "Promise", params):
+				builder.add("async ");
+			default:
+		}
+		builder.add("-> ");
+		switch tret {
+			case TAbstract(_.get().name => "Promise", params):
+				builder.add(getSwiftType(params[0]));
+			default:
+				builder.add(getSwiftType(tret));
+		}
+		if (genBody) {
+			builder.add(" {\n\t\t");
+			switch tret {
+				case TAbstract(_.get().name => "Promise", params):
+				builder.add("return await withUnsafeContinuation { cont in\n\t\t");
+				builder.add("let __cont_ptr = UnsafeMutableRawPointer(Unmanaged.passRetained(cont as AnyObject).toOpaque())\n\t\t");
+				default:
+			}
+			for (arg in targs) {
+				switch (arg.t) {
+				case TFun(fargs, fret):
+					builder.add("let __");
+					builder.add(arg.name);
+					builder.add("_ptr = UnsafeMutableRawPointer(Unmanaged.passRetained(");
+					builder.add(arg.name);
+					builder.add(" as AnyObject).toOpaque())\n\t\t");
+				default:
+				}
+			}
+			for (arg in targs) {
+				final allowNull = switch arg.t {
+				case TAbstract(_.get().name => "Null", [param]): true;
+				default: false;
+				};
+				switch TypeTools.followWithAbstracts(Context.resolveType(Context.toComplexType(arg.t), Context.currentPos()), false) {
+				case TInst(_.get().name => "Array", [TInst(_.get().name => "String", _)]):
+				builder.add("with" + (allowNull ? "Optional" : "") + "ArrayOfCStrings(" + arg.name + ") { __" + arg.name + " in ");
+				default:
+				}
+			}
+			final ibuilder = new hx.strings.StringBuilder("c_");
+			ibuilder.add(libName);
+			ibuilder.add(".");
+			ibuilder.add(cFuncName);
+			ibuilder.add("(\n\t\t\tself.o");
+			for (arg in targs) {
+				ibuilder.add(",\n\t\t\t");
+				final allowNull = switch arg.t {
+				case TAbstract(_.get().name => "Null", [param]): true;
+				default: false;
+				};
+				switch TypeTools.followWithAbstracts(Context.resolveType(Context.toComplexType(arg.t), Context.currentPos()), false) {
+				case TFun(fargs, fret):
+					ibuilder.add("{ (");
+					for (i => farg in fargs) {
+						if (i > 0) ibuilder.add(", ");
+						ibuilder.add("a" + i);
+						switch (farg.t) {
+						case TInst(_.get().name => "Array", params):
+							ibuilder.add(", a" + i + "_length");
+						default:
+						}
+					}
+					if (fargs.length > 0) ibuilder.add(", ");
+					ibuilder.add("ctx");
+					// TODO unretained vs retained
+					ibuilder.add(") in\n\t\t\t\tlet ");
+					ibuilder.add(arg.name);
+					ibuilder.add(" = Unmanaged<AnyObject>.fromOpaque(ctx!).takeUnretainedValue() as! ");
+					ibuilder.add(getSwiftType(arg.t));
+					ibuilder.add("\n\t\t\t\t");
+					final cbuilder = new hx.strings.StringBuilder(arg.name);
+					cbuilder.add("(");
+					for (i => farg in fargs) {
+						if (i > 0) cbuilder.add(", ");
+						cbuilder.add(castToSwift("a" + i, farg.t));
+					}
+					cbuilder.add(")");
+					ibuilder.add("return ");
+					ibuilder.add(castToC(cbuilder.toString(), fret, false));
+					ibuilder.add("\n\t\t\t},\n\t\t\t__");
+					ibuilder.add(arg.name);
+					ibuilder.add("_ptr");
+				case TInst(_.get().name => "Array", [TInst(_.get().name => "String", _)]):
+					ibuilder.add("__");
+					ibuilder.add(arg.name);
+					ibuilder.add(", ");
+					ibuilder.add(arg.name + (allowNull ? "?" : "") + ".count" + (allowNull ? " ?? 0" : ""));
+				case TInst(_.get().name => "Array", [param]):
+					ibuilder.add(castToC(arg.name, arg.t));
+					ibuilder.add(", ");
+					ibuilder.add(arg.name + (allowNull ? "?" : "") + ".count" + (allowNull ? " ?? 0" : ""));
+				default:
+					ibuilder.add(castToC(arg.name, arg.t));
+				}
+			}
+			switch tret {
+				case TAbstract(_.get().name => "Promise", params):
+					ibuilder.add(",\n\t\t\t");
+					mkSwiftAsync(ibuilder, params[0]);
+					finalTret = Context.resolveType(TPath({name: "Void", pack: []}), Context.currentPos());
+				default:
+			}
+			ibuilder.add("\n\t\t)");
+			builder.add("return ");
+			builder.add(castToSwift(ibuilder.toString(), finalTret, false, true));
+			for (arg in targs) {
+				switch TypeTools.followWithAbstracts(Context.resolveType(Context.toComplexType(arg.t), Context.currentPos()), false) {
+				case TInst(_.get().name => "Array", [TInst(_.get().name => "String", _)]):
+				builder.add("}");
+				default:
+				}
+			}
+			switch tret {
+				case TAbstract(_.get().name => "Promise", params):
+				builder.add("\n\t\t}");
+				default:
+			}
+			builder.add("\n\t}");
+		}
+		builder.add("\n\n");
+	}
+
 	static function convertQueuedClass(clsRef: Ref<ClassType>, namespace: String, fields: Array<Field>) {
 		var cls = clsRef.get();
 
@@ -305,6 +499,7 @@ class HaxeSwiftBridge {
 				.concat(safeIdent(classPrefix.join('.')) != libName ? classPrefix : [])
 				.filter(s -> s != '');
 
+		final extensionBuilder = new hx.strings.StringBuilder("public extension " + cls.name + " {\n");
 		final builder = new hx.strings.StringBuilder(cls.isInterface ? "public protocol " : "public class ");
 		builder.add(cls.name);
 		final superClass = if (cls.superClass == null) {
@@ -346,7 +541,6 @@ class HaxeSwiftBridge {
 			final noemit = f.meta.extract("HaxeCBridge.noemit")[0];
 			var isConvertibleMethod = f.isPublic && !f.isExtern && (noemit == null || (noemit.params != null && noemit.params.length > 0));
 			if (!isConvertibleMethod) return;
-			if (cls.isInterface) return;
 			if (read != AccNormal && read != AccCall) return; // Swift doesn't allow write-only
 
 			final cNameMeta = getCNameMeta(f.meta);
@@ -357,60 +551,15 @@ class HaxeSwiftBridge {
 			final cleanDoc = f.doc != null ? StringTools.trim(removeIndentation(f.doc)) : null;
 			if (cleanDoc != null) builder.add('\t/**\n${cleanDoc.split('\n').map(l -> '\t ' + l).join('\n')}\n\t */\n');
 
-			builder.add("\tpublic var ");
-			builder.add(f.name);
-			builder.add(": ");
-			builder.add(getSwiftType(f.type));
-			builder.add(" {\n");
-			if (read == AccNormal || read == AccCall) {
-				builder.add("\t\tget {\n\t\t\t");
-				builder.add(castToSwift('c_${libName}.${cFuncNameGet}(${isStatic ? '' : 'o'})', f.type, false, true));
-				builder.add("\n\t\t}\n");
-			}
-			if (write == AccNormal || write == AccCall) {
-				builder.add("\t\tset {\n\t\t\tc_");
-				builder.add(libName);
-				builder.add(".");
-				builder.add(cFuncNameSet);
-				builder.add("(" + (isStatic ? "" : "o, "));
-				builder.add(castToC("newValue", f.type));
-				switch TypeTools.followWithAbstracts(Context.resolveType(Context.toComplexType(f.type), Context.currentPos()), false) {
-				case TInst(_.get().name => "Array", [param]):
-					builder.add(", ");
-					builder.add("newValue.count");
-				default:
-				}
-				builder.add(")\n\t\t}\n");
-			}
-			builder.add("\t}\n\n");
-		}
 
-		function mkSwiftAsync(ibuilder: hx.strings.StringBuilder, ret: haxe.macro.Type) {
-			ibuilder.add("{ (");
-			ibuilder.add("a");
-			switch (ret) {
-			case TInst(_.get().name => "Array", params):
-				ibuilder.add(", a_length");
-			default:
-			}
-			ibuilder.add(", ctx");
-			ibuilder.add(") in\n\t\t\t\tlet cont = Unmanaged<AnyObject>.fromOpaque(ctx!).takeRetainedValue() as! UnsafeContinuation<");
-			ibuilder.add(getSwiftType(ret));
-			ibuilder.add(", Never>\n\t\t\t\t");
-			final cbuilder = new hx.strings.StringBuilder("cont.resume");
-			cbuilder.add("(returning: ");
-			cbuilder.add(castToSwift("a", ret));
-			cbuilder.add(")");
-			ibuilder.add(cbuilder.toString());
-			ibuilder.add("\n\t\t\t},\n\t\t\t__");
-			ibuilder.add("cont_ptr");
+			buildVar(f, libName, cFuncNameGet, cFuncNameSet, read, write, isStatic, builder, !cls.isInterface, !cls.isInterface);
+			if (cls.isInterface) buildVar(f, libName, cFuncNameGet, cFuncNameSet, read, write, isStatic, extensionBuilder, true, false);
 		}
 
 		function convertFunction(f: ClassField, kind: SwiftFunctionInfoKind, ?fld: Field) {
 			final noemit = f.meta.extract("HaxeCBridge.noemit")[0];
 			var isConvertibleMethod = f.isPublic && !f.isExtern && !f.meta.has("HaxeCBridge.wrapper") && (noemit == null || (noemit.params != null && noemit.params.length > 0));
 			if (!isConvertibleMethod) return;
-			if (cls.isInterface) return;
 			switch f.type {
 				case TFun(targs, tret):
 					final cNameMeta = getCNameMeta(f.meta);
@@ -463,131 +612,8 @@ class HaxeSwiftBridge {
 							}
 							builder.add("))\n\t}\n\n");
 						case Member:
-							builder.add("\tpublic func ");
-							builder.add(funcName);
-							builder.add("(");
-							convertArgs(builder, targs, fld?.kind);
-							builder.add(") ");
-							switch tret {
-								case TAbstract(_.get().name => "Promise", params):
-									builder.add("async ");
-								default:
-							}
-							builder.add("-> ");
-							switch tret {
-								case TAbstract(_.get().name => "Promise", params):
-									builder.add(getSwiftType(params[0]));
-								default:
-									builder.add(getSwiftType(tret));
-							}
-							builder.add(" {\n\t\t");
-							switch tret {
-								case TAbstract(_.get().name => "Promise", params):
-								builder.add("return await withUnsafeContinuation { cont in\n\t\t");
-								builder.add("let __cont_ptr = UnsafeMutableRawPointer(Unmanaged.passRetained(cont as AnyObject).toOpaque())\n\t\t");
-								default:
-							}
-							for (arg in targs) {
-								switch (arg.t) {
-								case TFun(fargs, fret):
-									builder.add("let __");
-									builder.add(arg.name);
-									builder.add("_ptr = UnsafeMutableRawPointer(Unmanaged.passRetained(");
-									builder.add(arg.name);
-									builder.add(" as AnyObject).toOpaque())\n\t\t");
-								default:
-								}
-							}
-							for (arg in targs) {
-								final allowNull = switch arg.t {
-								case TAbstract(_.get().name => "Null", [param]): true;
-								default: false;
-								};
-								switch TypeTools.followWithAbstracts(Context.resolveType(Context.toComplexType(arg.t), Context.currentPos()), false) {
-								case TInst(_.get().name => "Array", [TInst(_.get().name => "String", _)]):
-								builder.add("with" + (allowNull ? "Optional" : "") + "ArrayOfCStrings(" + arg.name + ") { __" + arg.name + " in ");
-								default:
-								}
-							}
-							final ibuilder = new hx.strings.StringBuilder("c_");
-							ibuilder.add(libName);
-							ibuilder.add(".");
-							ibuilder.add(cFuncName);
-							ibuilder.add("(\n\t\t\tself.o");
-							for (arg in targs) {
-								ibuilder.add(",\n\t\t\t");
-								final allowNull = switch arg.t {
-								case TAbstract(_.get().name => "Null", [param]): true;
-								default: false;
-								};
-								switch TypeTools.followWithAbstracts(Context.resolveType(Context.toComplexType(arg.t), Context.currentPos()), false) {
-								case TFun(fargs, fret):
-									ibuilder.add("{ (");
-									for (i => farg in fargs) {
-										if (i > 0) ibuilder.add(", ");
-										ibuilder.add("a" + i);
-										switch (farg.t) {
-										case TInst(_.get().name => "Array", params):
-											ibuilder.add(", a" + i + "_length");
-										default:
-										}
-									}
-									if (fargs.length > 0) ibuilder.add(", ");
-									ibuilder.add("ctx");
-									// TODO unretained vs retained
-									ibuilder.add(") in\n\t\t\t\tlet ");
-									ibuilder.add(arg.name);
-									ibuilder.add(" = Unmanaged<AnyObject>.fromOpaque(ctx!).takeUnretainedValue() as! ");
-									ibuilder.add(getSwiftType(arg.t));
-									ibuilder.add("\n\t\t\t\t");
-									final cbuilder = new hx.strings.StringBuilder(arg.name);
-									cbuilder.add("(");
-									for (i => farg in fargs) {
-										if (i > 0) cbuilder.add(", ");
-										cbuilder.add(castToSwift("a" + i, farg.t));
-									}
-									cbuilder.add(")");
-									ibuilder.add("return ");
-									ibuilder.add(castToC(cbuilder.toString(), fret, false));
-									ibuilder.add("\n\t\t\t},\n\t\t\t__");
-									ibuilder.add(arg.name);
-									ibuilder.add("_ptr");
-								case TInst(_.get().name => "Array", [TInst(_.get().name => "String", _)]):
-									ibuilder.add("__");
-									ibuilder.add(arg.name);
-									ibuilder.add(", ");
-									ibuilder.add(arg.name + (allowNull ? "?" : "") + ".count" + (allowNull ? " ?? 0" : ""));
-								case TInst(_.get().name => "Array", [param]):
-									ibuilder.add(castToC(arg.name, arg.t));
-									ibuilder.add(", ");
-									ibuilder.add(arg.name + (allowNull ? "?" : "") + ".count" + (allowNull ? " ?? 0" : ""));
-								default:
-									ibuilder.add(castToC(arg.name, arg.t));
-								}
-							}
-							switch tret {
-								case TAbstract(_.get().name => "Promise", params):
-									ibuilder.add(",\n\t\t\t");
-									mkSwiftAsync(ibuilder, params[0]);
-									finalTret = Context.resolveType(TPath({name: "Void", pack: []}), Context.currentPos());
-								default:
-							}
-							ibuilder.add("\n\t\t)");
-							builder.add("return ");
-							builder.add(castToSwift(ibuilder.toString(), finalTret, false, true));
-							for (arg in targs) {
-								switch TypeTools.followWithAbstracts(Context.resolveType(Context.toComplexType(arg.t), Context.currentPos()), false) {
-								case TInst(_.get().name => "Array", [TInst(_.get().name => "String", _)]):
-								builder.add("}");
-								default:
-								}
-							}
-							switch tret {
-								case TAbstract(_.get().name => "Promise", params):
-								builder.add("\n\t\t}");
-								default:
-							}
-							builder.add("\n\t}\n\n");
+							buildMember(funcName, cFuncName, fld, targs, tret, builder, !cls.isInterface, !cls.isInterface);
+							if (cls.isInterface) buildMember(funcName, cFuncName, fld, targs, tret, extensionBuilder, true, false);
 						case Static:
 							builder.add("\tpublic static func ");
 							builder.add(funcName);
@@ -682,7 +708,9 @@ class HaxeSwiftBridge {
 		for (f in cls.fields.get()) {
 			switch (f.kind) {
 			case FMethod(MethMacro):
-			case FMethod(_): convertFunction(f, Member, fields.find(fld -> f.name == fld.name));
+			case FMethod(_):
+				final fld = fields.find(fld -> f.name == fld.name);
+				if (fld != null) convertFunction(f, Member, fld);
 			case FVar(read, write): convertVar(f, read, write);
 			}
 		}
@@ -698,7 +726,10 @@ class HaxeSwiftBridge {
 		builder.add("}\n");
 
 		if (cls.isInterface) {
-			// TODO: extension with defaults for all exposed methods
+			extensionBuilder.add("}\n");
+			builder.add("\n");
+			builder.add(extensionBuilder.toString());
+
 			builder.add("\npublic class Any");
 			builder.add(cls.name);
 			builder.add(": ");
@@ -757,7 +788,7 @@ class HaxeSwiftBridge {
 				c_' + libName + '.' + libName + '_stop(wait)
 			}
 
-			public protocol SDKObject {
+			public protocol SDKObject: Sendable {
 				var o: UnsafeMutableRawPointer {get}
 			}
 
diff --git a/borogove/Form.hx b/borogove/Form.hx
index 143b863..05b3662 100644
--- a/borogove/Form.hx
+++ b/borogove/Form.hx
@@ -8,6 +8,7 @@ import HaxeCBridge;
 
 @:expose
 #if cpp
+@:build(HaxeCBridge.expose())
 @:build(HaxeSwiftBridge.expose())
 #end
 interface FormSection {
diff --git a/borogove/Persistence.hx b/borogove/Persistence.hx
index e089d4f..f167df7 100644
--- a/borogove/Persistence.hx
+++ b/borogove/Persistence.hx
@@ -6,24 +6,31 @@ import borogove.ChatMessage;
 import borogove.Message;
 import thenshim.Promise;
 
+#if cpp
+import HaxeCBridge;
+#end
+
 #if !NO_OMEMO
 import borogove.OMEMO;
 using borogove.SignalProtocol;
 #end
 
 #if cpp
+@:build(HaxeCBridge.expose())
 @:build(HaxeSwiftBridge.expose())
 #end
 interface Persistence {
 	public function lastId(accountId: String, chatId: Null<String>): Promise<Null<String>>;
 	public function storeChats(accountId: String, chats: Array<Chat>):Void;
+	@HaxeCBridge.noemit
 	public function getChats(accountId: String): Promise<Array<SerializedChat>>;
 	@HaxeCBridge.noemit
 	public function getChatsUnreadDetails(accountId: String, chats: Array<Chat>): Promise<Array<{ chatId: String, message: ChatMessage, unreadCount: Int }>>;
+	@HaxeCBridge.noemit
 	public function storeReaction(accountId: String, update: ReactionUpdate): Promise<Null<ChatMessage>>;
 	public function storeMessages(accountId: String, message: Array<ChatMessage>): Promise<Array<ChatMessage>>;
 	public function updateMessage(accountId: String, message: ChatMessage):Void;
-	public function updateMessageStatus(accountId: String, localId: String, status:MessageStatus, statusText: Null<String>): Promise<ChatMessage>;
+	public function updateMessageStatus(accountId: String, localId: String, status:borogove.Message.MessageStatus, statusText: Null<String>): Promise<ChatMessage>;
 	public function getMessage(accountId: String, chatId: String, serverId: Null<String>, localId: Null<String>): Promise<Null<ChatMessage>>;
 	public function getMessagesBefore(accountId: String, chatId: String, beforeId: Null<String>, beforeTime: Null<String>): Promise<Array<ChatMessage>>;
 	public function getMessagesAfter(accountId: String, chatId: String, afterId: Null<String>, afterTime: Null<String>): Promise<Array<ChatMessage>>;
@@ -31,14 +38,20 @@ interface Persistence {
 	public function hasMedia(hashAlgorithm:String, hash:BytesData): Promise<Bool>;
 	public function storeMedia(mime:String, bytes:BytesData): Promise<Bool>;
 	public function removeMedia(hashAlgorithm:String, hash:BytesData):Void;
+	@HaxeCBridge.noemit
 	public function storeCaps(caps:Caps):Void;
+	@HaxeCBridge.noemit
 	public function getCaps(ver:String): Promise<Null<Caps>>;
 	public function storeLogin(login:String, clientId:String, displayName:String, token:Null<String>):Void;
+	@HaxeCBridge.noemit
 	public function getLogin(login:String): Promise<{ clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String> }>;
 	public function removeAccount(accountId: String, completely:Bool):Void;
 	public function listAccounts(): Promise<Array<String>>;
+	@HaxeCBridge.noemit
 	public function storeStreamManagement(accountId:String, data:Null<BytesData>):Void;
+	@HaxeCBridge.noemit
 	public function getStreamManagement(accountId:String): Promise<Null<BytesData>>;
+	@HaxeCBridge.noemit
 	public function storeService(accountId:String, serviceId:String, name:Null<String>, node:Null<String>, caps:Caps):Void;
 	@HaxeCBridge.noemit
 	public function findServicesWithFeature(accountId:String, feature:String): Promise<Array<{serviceId:String, name:Null<String>, node:Null<String>, caps: Caps}>>;
diff --git a/borogove/calls/Session.hx b/borogove/calls/Session.hx
index 6d8b0a5..ad61083 100644
--- a/borogove/calls/Session.hx
+++ b/borogove/calls/Session.hx
@@ -23,6 +23,7 @@ enum abstract CallStatus(Int) {
 }
 
 #if cpp
+@:build(HaxeCBridge.expose())
 @:build(HaxeSwiftBridge.expose())
 #end
 @:expose
@@ -47,7 +48,7 @@ interface Session {
 	public function callStatus():CallStatus;
 	public function audioTracks():Array<MediaStreamTrack>;
 	public function videoTracks():Array<MediaStreamTrack>;
-	public function dtmf():Null<DTMFSender>;
+	public function dtmf():Null<borogove.calls.PeerConnection.DTMFSender>;
 }
 
 private function mkCallMessage(to: JID, from: JID, event: Stanza) {
@@ -327,7 +328,9 @@ class OutgoingProposedSession implements Session {
 #end
 @:expose
 class InitiatedSession implements Session {
+	@HaxeCBridge.noemit
 	public var sid (get, null): String;
+	@HaxeCBridge.noemit
 	public var chatId (get, null): String;
 	private final client: Client;
 	private final counterpart: JID;
@@ -383,6 +386,7 @@ class InitiatedSession implements Session {
 		trace("Tried to retract session in wrong state: " + sid, this);
 	}
 
+	@HaxeCBridge.noemit
 	public function accept() {
 		if (accepted || remoteDescription == null) return;
 		accepted = true;
@@ -391,6 +395,7 @@ class InitiatedSession implements Session {
 		client.trigger("call/media", { session: this, audio: audio, video: video });
 	}
 
+	@HaxeCBridge.noemit
 	public function hangup() {
 		client.sendStanza(
 			new Stanza("iq", { to: counterpart.asString(), type: "set", id: ID.medium() })
@@ -504,6 +509,7 @@ class InitiatedSession implements Session {
 		})).then((_) -> {});
 	}
 
+	@HaxeCBridge.noemit
 	public function addMedia(streams: Array<MediaStream>): Void {
 		if (pc == null) throw "tried to add media before PeerConnection exists";
 
@@ -517,6 +523,7 @@ class InitiatedSession implements Session {
 		setupLocalDescription("content-add", oldMids, true);
 	}
 
+	@HaxeCBridge.noemit
 	public function callStatus() {
 		return if (pc == null || pc.connectionState == "connecting" || pc.connectionState == "new") {
 			Connecting;
@@ -527,6 +534,7 @@ class InitiatedSession implements Session {
 		}
 	}
 
+	@HaxeCBridge.noemit
 	public function audioTracks(): Array<MediaStreamTrack> {
 		if (pc == null) return [];
 		return pc.getTransceivers()
@@ -534,6 +542,7 @@ class InitiatedSession implements Session {
 			.map((t) -> t.receiver.track);
 	}
 
+	@HaxeCBridge.noemit
 	public function videoTracks(): Array<MediaStreamTrack> {
 		if (pc == null) return [];
 		return pc.getTransceivers()
@@ -541,6 +550,7 @@ class InitiatedSession implements Session {
 			.map((t) -> t.receiver.track);
 	}
 
+	@HaxeCBridge.noemit
 	public function dtmf() {
 		if (pc == null) return null;
 		final transceiver = pc.getTransceivers().find((t) -> t.sender != null && t.sender.track != null && t.sender.track.kind == "audio" && !t.sender.track.muted);
diff --git a/borogove/persistence/KeyValueStore.hx b/borogove/persistence/KeyValueStore.hx
index 84bd59c..05eb9d0 100644
--- a/borogove/persistence/KeyValueStore.hx
+++ b/borogove/persistence/KeyValueStore.hx
@@ -3,6 +3,11 @@ package borogove.persistence;
 import thenshim.Promise;
 
 #if cpp
+import HaxeCBridge;
+#end
+
+#if cpp
+@:build(HaxeCBridge.expose())
 @:build(HaxeSwiftBridge.expose())
 #end
 interface KeyValueStore {
diff --git a/borogove/persistence/MediaStore.hx b/borogove/persistence/MediaStore.hx
index 368020d..65b45dd 100644
--- a/borogove/persistence/MediaStore.hx
+++ b/borogove/persistence/MediaStore.hx
@@ -4,6 +4,11 @@ import thenshim.Promise;
 import haxe.io.BytesData;
 
 #if cpp
+import HaxeCBridge;
+#end
+
+#if cpp
+@:build(HaxeCBridge.expose())
 @:build(HaxeSwiftBridge.expose())
 #end
 interface MediaStore {