git » sdk » commit 444096b

Automatic swift bindings

author Stephen Paul Weber
2024-03-25 15:24:35 UTC
committer Stephen Paul Weber
2024-03-25 15:24:35 UTC
parent 3086192d01d304cccc3c4cb4caff2d60291d69d4

Automatic swift bindings

HaxeSwiftBridge.hx +626 -0
snikket/Chat.hx +4 -0
snikket/ChatMessage.hx +14 -3
snikket/Client.hx +2 -1
snikket/Persistence.hx +5 -0
snikket/jingle/PeerConnection.cpp.hx +20 -2
snikket/jingle/Session.hx +3 -0
snikket/persistence/Sqlite.hx +3 -0

diff --git a/HaxeSwiftBridge.hx b/HaxeSwiftBridge.hx
new file mode 100644
index 0000000..f103bf3
--- /dev/null
+++ b/HaxeSwiftBridge.hx
@@ -0,0 +1,626 @@
+#if (haxe_ver < 4.0) #error "Haxe 4.0 required" #end
+
+#if macro
+
+	// fast path for when code gen isn't required
+	// disable this to get auto-complete when editing this file
+	#if (display || display_details || !sys || cppia)
+
+class HaxeSwiftBridge {
+	public static function expose(?namespace: String)
+		return haxe.macro.Context.getBuildFields();
+	@:noCompletion
+	static macro function runUserMain()
+		return macro null;
+}
+
+	#else
+
+import HaxeCBridge.CodeTools.*;
+import haxe.ds.ReadOnlyArray;
+import haxe.io.Path;
+import haxe.macro.Compiler;
+import haxe.macro.ComplexTypeTools;
+import haxe.macro.Context;
+import haxe.macro.Expr;
+import haxe.macro.ExprTools;
+import haxe.macro.PositionTools;
+import haxe.macro.Printer;
+import haxe.macro.Type;
+import haxe.macro.TypeTools;
+import haxe.macro.TypedExprTools;
+import sys.FileSystem;
+import sys.io.File;
+
+using Lambda;
+using StringTools;
+
+class HaxeSwiftBridge {
+
+	static final noOutput = Sys.args().has('--no-output');
+	static final printer = new Printer();
+	
+	static var firstRun = true;
+
+	static var libName: Null<String> = getLibNameFromHaxeArgs(); // null if no libName determined from args
+
+	static final compilerOutputDir = Compiler.getOutput();
+	// paths relative to the compiler output directory
+	static final implementationPath = Path.join([hx.strings.Strings.toUpperCamel(libName) + '.swift']);
+	
+	static final queuedClasses = new Array<{
+		cls: Ref<ClassType>,
+		namespace: String,
+	}>();
+
+	static final knownEnums: Map<String,String> = [];
+
+	static public function expose(?namespace: String) {
+		var clsRef = Context.getLocalClass(); 
+		var cls = clsRef.get();
+		var fields = Context.getBuildFields();
+
+		if (libName == null) {
+			// if we cannot determine a libName from --main or -D, we use the first exposed class
+			libName = if (namespace != null) {
+				namespace;
+			} else {
+				cls.name;
+			}
+		}
+
+		queuedClasses.push({
+			cls: clsRef,
+			namespace: namespace
+		});
+
+		// add @:keep
+		cls.meta.add(':keep', [], Context.currentPos());
+
+		if (firstRun) {
+			var HaxeSwiftBridgeType = Context.resolveType(macro :HaxeSwiftBridge, Context.currentPos());
+			switch HaxeSwiftBridgeType {
+				case TInst(_.get().meta => meta, params):
+					if (!meta.has(':buildXml')) {
+						meta.add(':buildXml', [
+							macro $v{code('
+								<!-- HaxeSwiftBridge -->
+								<files id="haxe">
+									<depend name="$implementationPath"/>
+								</files>
+							')}
+						], Context.currentPos());
+					}
+				default: throw 'Internal error';
+			}
+
+			Context.onAfterTyping(_ -> {
+				var implementation = generateImplementation();
+
+				function saveFile(path: String, content: String) {
+					var directory = Path.directory(path);
+					if (!FileSystem.exists(directory)) {
+						FileSystem.createDirectory(directory);
+					}
+					// only save if there's a difference (save C++ compilation by not changing the file if not needed)
+					if (FileSystem.exists(path)) {
+						if (content == sys.io.File.getContent(path)) {
+							return;
+						}
+					}
+					sys.io.File.saveContent(path, content);	
+				}
+
+				if (!noOutput) {
+					saveFile(Path.join([compilerOutputDir, implementationPath]), implementation);
+				}
+			});
+
+			firstRun = false;
+		}
+
+		return fields;
+	}
+
+	static function getHxcppNativeName(t: BaseType) {
+		var nativeMeta = t.meta.extract(':native')[0];
+		var nativeMetaValue = nativeMeta != null ? ExprTools.getValue(nativeMeta.params[0]) : null;
+		var nativeName = (nativeMetaValue != null ? nativeMetaValue : t.pack.concat([t.name]).join('.'));
+		return nativeName;
+	}
+
+	static function getSwiftType(type, arg = false) {
+		return switch type {
+		case TInst(_.get().name => "String", params):
+			return "String";
+		case TInst(_.get().name => "Array", [param]):
+			return "Array<" + getSwiftType(param, arg) + ">";
+		case TInst(_.get() => t, []):
+			return t.name;
+		case TAbstract(_.get().name => "Null", [param]):
+			return getSwiftType(param) + "?";
+		case TAbstract(_.get().name => "Int", []):
+			return "Int32";
+		case TAbstract(_.get() => t, []):
+			final isPublicEnumAbstract = t.meta.has(':enum') && !t.isPrivate;
+			final isIntEnumAbstract = if (isPublicEnumAbstract) {
+				final underlyingRootType = TypeTools.followWithAbstracts(t.type, false);
+				Context.unify(underlyingRootType, Context.resolveType(macro :Int, Context.currentPos()));
+			} else false;
+			if (isIntEnumAbstract) {
+				knownEnums[t.name] = hx.strings.Strings.toLowerUnderscore(safeIdent(TypeTools.toString(type)));
+			}
+			return t.name;
+		case TFun(args, ret):
+			final builder = new hx.strings.StringBuilder(arg ? "@escaping (" : "(");
+			for (i => arg in args) {
+				if (i > 0) builder.add(", ");
+				builder.add(getSwiftType(arg.t));
+			}
+			builder.add(")->");
+			builder.add(getSwiftType(ret));
+
+			return builder.toString();
+		case TType(_.get() => t, params):
+			return getSwiftType(TypeTools.follow(type, true), arg);
+		default:
+			Context.fatalError("No implemented Swift type conversion for: " + type, Context.currentPos());
+		}
+	}
+
+	static function convertArgs(builder: hx.strings.StringBuilder, args: Array<{ name: String, opt: Bool, t: haxe.macro.Type }>) {
+		for (i => arg in args) {
+			if (i > 0) builder.add(", ");
+			builder.add(arg.name);
+			builder.add(": ");
+			builder.add(getSwiftType(arg.t, true));
+			if (arg.opt) {
+				Context.fatalError("default? " + arg.name, Context.currentPos());
+			}
+		}
+	}
+
+	static function castToSwift(item: String, type: haxe.macro.Type, canNull = false, isRet = false) {
+		return switch type {
+		case TInst(_.get().name => "String", params):
+			return "useString(" + item + ")" + (canNull ? "" : "!");
+		case TInst(_.get().name => "Array", [param]):
+			if (isRet) {
+				return
+					"{" +
+					"var __ret: UnsafeMutablePointer<UnsafeMutableRawPointer?>? = nil;" +
+					"let __ret_length = " + ~/\)$/.replace(item, ", &__ret);") +
+					"return " + castToSwift("__ret", type, canNull, false) + ";" +
+					"}()";
+			} else {
+				return
+					"{" +
+					"let __r = UnsafeMutableBufferPointer<UnsafeMutableRawPointer?>(start: " + item + ", count: " + item + "_length).map({" +
+					castToSwift("$0", param) +
+					"});" +
+					"c_" + libName + "." + libName + "_release(" + item + ");" +
+					"return __r;" +
+					"}()";
+			}
+		case TInst(_.get() => t, []):
+			final wrapper = t.isInterface ? 'Any${t.name}' : t.name;
+			if (canNull) {
+				return "(" + item + ").map({ " + wrapper + "($0) })";
+			} else {
+				return wrapper + "(" + item + "!)";
+			}
+		case TAbstract(_.get().name => "Null", [param]):
+			return castToSwift(item, param, true);
+		case TAbstract(_.get() => t, []):
+			return item;
+		case TType(_.get() => t, params):
+			return castToSwift(item, TypeTools.follow(type, true), canNull);
+		default:
+			Context.fatalError("No implemented Swift cast for: " + type, Context.currentPos());
+		}
+	}
+
+	static function castToC(item: String, type: haxe.macro.Type, canNull = false) {
+		return switch type {
+		case TInst(_.get().name => "String", params):
+			return item;
+		case TInst(_.get().name => "Array", params):
+			return item;
+		case TInst(_.get() => t, []):
+			return item + (canNull ? "?" : "") + ".o";
+		case TAbstract(_.get().name => "Null", [param]):
+			return castToC(item, param, true);
+		case TAbstract(_.get() => t, []):
+			return item;
+		case TType(_.get() => t, params):
+			return castToC(item, TypeTools.follow(type, true));
+		default:
+			Context.fatalError("No implemented C cast for: " + type, Context.currentPos());
+		}
+	}
+
+	static function metaIsSwiftExpose(e: ExprDef) {
+		return switch (e) {
+			case ECall(call, args):
+			switch (call.expr) {
+			case EField(l, r):
+				switch (l.expr) {
+				case EConst(CIdent("HaxeSwiftBridge")):
+					r == "expose";
+				default: false;
+				}
+			default: false;
+			}
+		default: false;
+		}
+	}
+
+	static function convertQueuedClass(clsRef: Ref<ClassType>, namespace: String) {
+		var cls = clsRef.get();
+
+		// validate
+		if (cls.isExtern) Context.error('Cannot expose extern directly to Swift', cls.pos);
+
+		var classPrefix = cls.pack.concat([namespace == null ? cls.name : namespace]);
+		var cNameMeta = getCNameMeta(cls.meta);
+
+		var functionPrefix =
+			if (cNameMeta != null)
+				[cNameMeta];
+			else 
+				[false ? libName : ""] // NOTE: probably want this if we get packages with differnt name?
+				.concat(safeIdent(classPrefix.join('.')) != libName ? classPrefix : [])
+				.filter(s -> s != '');
+
+		final builder = new hx.strings.StringBuilder(cls.isInterface ? "protocol " : "class ");
+		builder.add(cls.name);
+		final superClass = if (cls.superClass == null) {
+			null;
+		} else {
+			final buildMeta = cls.superClass.t.get().meta.extract(":build");
+			if (buildMeta.exists(meta -> metaIsSwiftExpose(meta.params[0]?.expr))) {
+				cls.superClass.t.get();
+			} else {
+				null;
+			}
+		}
+		if (superClass == null) {
+			builder.add(": SDKObject");
+		} else {
+			builder.add(": ");
+			builder.add(superClass.name);
+		}
+		for (iface in cls.interfaces) {
+			builder.add(", ");
+			builder.add(iface.t.get().name);
+		}
+
+		builder.add(" {\n");
+		if (!cls.isInterface && superClass == null) {
+			builder.add("\tinternal let o: UnsafeMutableRawPointer\n\n\tinternal init(_ ptr: UnsafeMutableRawPointer) {\n\t\to = ptr\n\t}\n\n");
+		}
+
+		function convertVar(f: ClassField, read: VarAccess, write: VarAccess) {
+			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;
+
+			final cNameMeta = getCNameMeta(f.meta);
+
+			final cFuncNameGet = hx.strings.Strings.toLowerUnderscore(functionPrefix.concat([f.name]).join('_'));
+			final cFuncNameSet = hx.strings.Strings.toLowerUnderscore(functionPrefix.concat(["set", f.name]).join('_'));
+
+			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("\tvar ");
+			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}(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("(o, ");
+				builder.add(castToC("newValue", f.type));
+				builder.add(")\n\t\t}\n");
+			}
+			builder.add("\t}\n\n");
+		}
+
+		function convertFunction(f: ClassField, kind: SwiftFunctionInfoKind) {
+			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);
+
+					var cFuncName: String =
+						if (cNameMeta != null)
+							cNameMeta;
+						else if (f.meta.has("HaxeCBridge.wrapper"))
+							functionPrefix.concat([f.name.substring(0, f.name.length - 7)]).join('_');
+						else
+							functionPrefix.concat([f.name]).join('_');
+
+					var funcName: String =
+						if (f.meta.has("HaxeCBridge.wrapper"))
+							f.name.substring(0, f.name.length - 7);
+						else
+							f.name;
+
+					cFuncName = hx.strings.Strings.toLowerUnderscore(cFuncName);
+
+					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');
+					switch kind {
+						case Constructor:
+							builder.add("\tinit(");
+							convertArgs(builder, targs);
+							builder.add(") {\n\t\to = c_");
+							builder.add(libName);
+							builder.add(".");
+							builder.add(cFuncName);
+							builder.add("(");
+							for (i => arg in targs) {
+								if (i > 0) builder.add(", ");
+								builder.add(castToC(arg.name, arg.t));
+							}
+							builder.add(")\n\t}\n\n");
+						case Member:
+							builder.add("\tfunc ");
+							builder.add(funcName);
+							builder.add("(");
+							convertArgs(builder, targs);
+							builder.add(") -> ");
+							builder.add(getSwiftType(tret));
+							builder.add(" {\n\t\t");
+							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:
+								}
+							}
+							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");
+								switch (arg.t) {
+								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(castToSwift(cbuilder.toString(), fret, false, true));
+									ibuilder.add("\n\t\t\t},\n\t\t\t__");
+									ibuilder.add(arg.name);
+									ibuilder.add("_ptr");
+								default:
+									ibuilder.add(castToC(arg.name, arg.t));
+								}
+							}
+							ibuilder.add("\n\t\t)");
+							builder.add(castToSwift(ibuilder.toString(), tret, false, true));
+							builder.add("\n\t}\n\n");
+						case Static:
+							Context.fatalError('Swift bridging for statics not implemented yet', f.pos);
+					}
+
+				default: Context.fatalError('Internal error: Expected function expression', f.pos);
+			}
+		}
+
+		if (cls.constructor != null) {
+			convertFunction(cls.constructor.get(), Constructor);
+		}
+
+		for (f in cls.statics.get()) {
+			convertFunction(f, Static);
+		}
+
+		for (f in cls.fields.get()) {
+			switch (f.kind) {
+			case FMethod(MethMacro):
+			case FMethod(_): convertFunction(f, Member);
+			case FVar(read, write): convertVar(f, read, write);
+			}
+		}
+
+		if (!cls.isInterface && superClass == null) {
+			builder.add("\tdeinit {\n\t\tc_");
+			builder.add(libName);
+			builder.add(".");
+			builder.add(libName);
+			builder.add("_release(o)\n\t}\n");
+		}
+
+		builder.add("}\n");
+
+		if (cls.isInterface) {
+			// TODO: extension with defaults for all exposed methods
+			builder.add("\nclass Any");
+			builder.add(cls.name);
+			builder.add(": ");
+			builder.add(cls.name);
+			builder.add(" {\n");
+			builder.add("\tinternal let o: UnsafeMutableRawPointer\n\n\tinternal init(_ ptr: UnsafeMutableRawPointer) {\n\t\to = ptr\n\t}\n\n");
+			builder.add("\tdeinit {\n\t\tc_");
+			builder.add(libName);
+			builder.add(".");
+			builder.add(libName);
+			builder.add("_release(o)\n\t}\n");
+			builder.add("\n}\n");
+		}
+
+		return builder.toString();
+	}
+
+	static macro function runUserMain() {
+		var mainClassPath = getMainFromHaxeArgs(Sys.args());
+		if (mainClassPath == null) {
+			return macro null;
+		} else {
+			return Context.parse('$mainClassPath.main()', Context.currentPos());
+		}
+	}
+
+	static function isLibraryBuild() {
+		return Context.defined('dll_link') || Context.defined('static_link');
+	}
+
+	static function isDynamicLink() {
+		return Context.defined('dll_link');
+	}
+
+	static function getCNameMeta(meta: MetaAccess): Null<String> {
+		var cNameMeta = meta.extract('HaxeCBridge.name')[0];
+		return if (cNameMeta != null) {
+			switch cNameMeta.params {
+				case [{expr: EConst(CString(name))}]:
+					safeIdent(name);
+				default:
+					Context.error('Incorrect usage, syntax is @${cNameMeta.name}(name: String)', cNameMeta.pos);
+			}
+		} else null;
+	}
+
+	static function generateImplementation() {
+		return code('
+			import c_' + libName + '
+
+			func setup(_ handler: @convention(c) @escaping (UnsafePointer<CChar>?)->Void) {
+				c_' + libName + '.' + libName + '_setup(handler)
+			}
+
+			func stop(_ wait: Bool) {
+				c_' + libName + '.' + libName + '_stop(wait)
+			}
+
+			internal protocol SDKObject {
+				var o: UnsafeMutableRawPointer {get}
+			}
+
+			internal func useString(_ mptr: UnsafePointer<CChar>?) -> String? {
+				if let ptr = mptr {
+					let r = String(cString: ptr)
+					c_' + libName + '.' + libName + '_release(ptr)
+					return r
+				} else {
+					return nil
+				}
+			}
+
+			internal func useString(_ mptr: UnsafeMutableRawPointer?) -> String? {
+				return useString(UnsafePointer(mptr?.assumingMemoryBound(to: CChar.self)))
+			}
+
+		')
+		+ queuedClasses.map(c -> convertQueuedClass(c.cls, c.namespace)).join("\n") + "\n"
+		+ { iterator: () -> knownEnums.keyValueIterator() }.map(e -> "typealias " + e.key + " = " + e.value + "\n").join("\n")
+		;
+	}
+
+	/**
+		We determine a project name to be the `--main` startup class
+
+		The user can override this with `-D HaxeCBridge.name=ExampleName`
+
+		This isn't rigorously defined but hopefully will produced nicely namespaced and unsurprising function names
+	**/
+	static function getLibNameFromHaxeArgs(): Null<String> {
+		var overrideName = Context.definedValue('HaxeCBridge.name');
+		if (overrideName != null && overrideName != '') {
+			return safeIdent(overrideName);
+		}
+
+		var args = Sys.args();
+		
+		var mainClassPath = getMainFromHaxeArgs(args);
+		if (mainClassPath != null) {
+			return safeIdent(mainClassPath);
+		}
+
+		// no lib name indicator found in args
+		return null;
+	}
+
+	static function getMainFromHaxeArgs(args: Array<String>): Null<String> {
+		for (i in 0...args.length) {
+			var arg = args[i];
+			switch arg {
+				case '-m', '-main', '--main':
+					var classPath = args[i + 1];
+					return classPath;
+				default:
+			}
+		}
+		return null;
+	}
+
+	static function safeIdent(str: String) {
+		// replace non a-z0-9_ with _
+		str = ~/[^\w]/gi.replace(str, '_');
+		// replace leading number with _
+		str = ~/^[^a-z_]/i.replace(str, '_');
+		// replace empty string with _
+		str = str == '' ? '_' : str;
+		return str;
+	}
+
+}
+
+enum SwiftFunctionInfoKind {
+	Constructor;
+	Member;
+	Static;
+}
+
+	#end // (display || display_details || target.name != cpp)
+
+#elseif (cpp && !cppia)
+// runtime HaxeSwiftBridge
+
+class HaxeSwiftBridge {}
+
+#end
diff --git a/snikket/Chat.hx b/snikket/Chat.hx
index 366d7b1..7c905e9 100644
--- a/snikket/Chat.hx
+++ b/snikket/Chat.hx
@@ -34,6 +34,7 @@ class UiStateImpl {
 
 #if cpp
 @:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
 #end
 abstract class Chat {
 	private var client:Client;
@@ -390,6 +391,7 @@ abstract class Chat {
 @:expose
 #if cpp
 @:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
 #end
 class DirectChat extends Chat {
 	@:allow(snikket)
@@ -580,6 +582,7 @@ class DirectChat extends Chat {
 @:expose
 #if cpp
 @:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
 #end
 class Channel extends Chat {
 	@:allow(snikket)
@@ -968,6 +971,7 @@ class Channel extends Chat {
 @:expose
 #if cpp
 @:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
 #end
 class AvailableChat {
 	/**
diff --git a/snikket/ChatMessage.hx b/snikket/ChatMessage.hx
index 6bc4bda..06205f9 100644
--- a/snikket/ChatMessage.hx
+++ b/snikket/ChatMessage.hx
@@ -17,14 +17,22 @@ import snikket.XEP0393;
 import snikket.EmojiUtil;
 import snikket.Message;
 
+@:expose
+@:nullSafety(Strict)
+#if cpp
+@:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
+#end
 class ChatAttachment {
 	public final name: Null<String>;
 	public final mime: String;
 	public final size: Null<Int>;
 	public final uris: Array<String>;
+	@HaxeCBridge.noemit
 	public final hashes: Array<{algo:String, hash:BytesData}>;
 
-	public function new(name: Null<String>, mime: String, size: Null<Int>, uris: Array<String>, hashes: Array<{algo:String, hash:BytesData}>) {
+	@:allow(snikket)
+	private function new(name: Null<String>, mime: String, size: Null<Int>, uris: Array<String>, hashes: Array<{algo:String, hash:BytesData}>) {
 		this.name = name;
 		this.mime = mime;
 		this.size = size;
@@ -37,6 +45,7 @@ class ChatAttachment {
 @:nullSafety(Strict)
 #if cpp
 @:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
 #end
 class ChatMessage {
 	/**
@@ -82,10 +91,11 @@ class ChatMessage {
 	/**
 		Array of attachments to this message
 	**/
-	public var attachments: Array<ChatAttachment> = [];
+	public var attachments (default, null): Array<ChatAttachment> = [];
 	/**
 		Map of reactions to this message
 	**/
+	@HaxeCBridge.noemit
 	public var reactions: Map<String, Array<String>> = [];
 
 	/**
@@ -115,7 +125,8 @@ class ChatMessage {
 	/**
 		Array of past versions of this message, if it has been edited
 	**/
-	public var versions: Array<ChatMessage> = [];
+	@:allow(snikket)
+	public var versions (default, null): Array<ChatMessage> = [];
 	@:allow(snikket)
 	private var payloads: Array<Stanza> = [];
 
diff --git a/snikket/Client.hx b/snikket/Client.hx
index 83022ae..4390931 100644
--- a/snikket/Client.hx
+++ b/snikket/Client.hx
@@ -35,6 +35,7 @@ import HaxeCBridge;
 @:expose
 #if cpp
 @:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
 #end
 class Client extends EventEmitter {
 	private var stream:GenericStream;
@@ -440,7 +441,7 @@ class Client extends EventEmitter {
 	/**
 		Get the current display name for this account
 
-		@returns display name or NULL
+		@returns display name
 	**/
 	public function displayName() {
 		return _displayName;
diff --git a/snikket/Persistence.hx b/snikket/Persistence.hx
index 7cc9adf..8f945e3 100644
--- a/snikket/Persistence.hx
+++ b/snikket/Persistence.hx
@@ -5,10 +5,14 @@ import snikket.Chat;
 import snikket.ChatMessage;
 import snikket.Message;
 
+#if cpp
+@:build(HaxeSwiftBridge.expose())
+#end
 interface Persistence {
 	public function lastId(accountId: String, chatId: Null<String>, callback:(serverId:Null<String>)->Void):Void;
 	public function storeChat(accountId: String, chat: Chat):Void;
 	public function getChats(accountId: String, callback: (chats:Array<SerializedChat>)->Void):Void;
+	@HaxeCBridge.noemit
 	public function getChatsUnreadDetails(accountId: String, chats: Array<Chat>, callback: (details:Array<{ chatId: String, message: ChatMessage, unreadCount: Int }>)->Void):Void;
 	public function storeReaction(accountId: String, update: ReactionUpdate, callback: (Null<ChatMessage>)->Void):Void;
 	public function storeMessage(accountId: String, message: ChatMessage, callback: (ChatMessage)->Void):Void;
@@ -23,5 +27,6 @@ interface Persistence {
 	public function storeStreamManagement(accountId:String, smId:String, outboundCount:Int, inboundCount:Int, outboundQueue:Array<String>):Void;
 	public function getStreamManagement(accountId:String, callback: (smId:Null<String>, outboundCount:Int, inboundCount:Int, outboundQueue:Array<String>)->Void):Void;
 	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, callback:(Array<{serviceId:String, name:Null<String>, node:Null<String>, caps: Caps}>)->Void):Void;
 }
diff --git a/snikket/jingle/PeerConnection.cpp.hx b/snikket/jingle/PeerConnection.cpp.hx
index 58c7ef0..fa85cd0 100644
--- a/snikket/jingle/PeerConnection.cpp.hx
+++ b/snikket/jingle/PeerConnection.cpp.hx
@@ -1,12 +1,26 @@
 package snikket.jingle;
 
+#if cpp
+import HaxeCBridge;
+#end
+
 typedef TODO = Dynamic;
-typedef DTMFSender = TODO;
 typedef Transceiver = {
 	 receiver: Null<{ track: MediaStreamTrack }>,
 	 sender: Null<{ track: MediaStreamTrack, dtmf: DTMFSender }>
 }
 
+#if cpp
+@:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
+#end
+class DTMFSender {
+}
+
+#if cpp
+@:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
+#end
 class MediaStreamTrack {
 	 public var muted: Bool;
 	 public var kind: String;
@@ -14,8 +28,12 @@ class MediaStreamTrack {
 	 public function stop() { }
 }
 
+#if cpp
+@:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
+#end
 class MediaStream {
-	 public function getTracks() {
+	 public function getTracks(): Array<MediaStreamTrack> {
 		  return [];
 	 }
 }
diff --git a/snikket/jingle/Session.hx b/snikket/jingle/Session.hx
index fade99a..b1a753b 100644
--- a/snikket/jingle/Session.hx
+++ b/snikket/jingle/Session.hx
@@ -5,6 +5,9 @@ import snikket.jingle.PeerConnection;
 import snikket.jingle.SessionDescription;
 using Lambda;
 
+#if cpp
+@:build(HaxeSwiftBridge.expose())
+#end
 interface Session {
 	public var sid (get, null): String;
 	public function initiate(stanza: Stanza): InitiatedSession;
diff --git a/snikket/persistence/Sqlite.hx b/snikket/persistence/Sqlite.hx
index 2818aa1..e929542 100644
--- a/snikket/persistence/Sqlite.hx
+++ b/snikket/persistence/Sqlite.hx
@@ -20,7 +20,10 @@ import snikket.Message;
 // TODO: consider doing background threads for operations
 
 @:expose
+#if cpp
 @:build(HaxeCBridge.expose())
+@:build(HaxeSwiftBridge.expose())
+#end
 class Sqlite implements Persistence {
 	final db: Connection;
 	final blobpath: String;