| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2024-02-28 16:05:31 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2024-02-28 18:51:00 UTC |
| parent | b94ce57a40f7ae37f992f2a6a39b838e3c9d2875 |
| HaxeCBridge.hx | +2059 | -0 |
| cpp.hxml | +14 | -0 |
| xmpp/Caps.hx | +6 | -2 |
| xmpp/Chat.hx | +26 | -4 |
| xmpp/ChatMessage.hx | +12 | -0 |
| xmpp/Client.hx | +20 | -3 |
| xmpp/Message.hx | +3 | -2 |
| xmpp/Persistence.hx | +3 | -3 |
| xmpp/PubsubEvent.hx | +38 | -0 |
| xmpp/Push.hx | +2 | -2 |
| xmpp/Stream.cpp.hx | +5 | -0 |
| xmpp/jingle/IceServer.hx | +10 | -0 |
| xmpp/jingle/PeerConnection.cpp.hx | +107 | -0 |
| xmpp/jingle/Session.hx | +2 | -2 |
| xmpp/persistence/Sqlite.hx | +319 | -0 |
| xmpp/persistence/browser.js | +2 | -2 |
| xmpp/streams/XmppStropheStream.hx | +324 | -0 |
diff --git a/HaxeCBridge.hx b/HaxeCBridge.hx new file mode 100644 index 0000000..76cf7a2 --- /dev/null +++ b/HaxeCBridge.hx @@ -0,0 +1,2059 @@ +/** + HaxeCBridge + + HaxeCBridge is a @:build macro that enables calling haxe code from C by exposing classes via an automatically generated C header. + + Works with the hxcpp target and requires haxe 4.0 or newer + + @author George Corney (haxiomic) + @license MIT + + **Usage** + + Haxe-side: + - Add `@:build(HaxeCBridge.expose())` to classes you want to expose to C (you can add this to as many classes as you like – all functions are combined into a single header file) + - The first argument of expose() sets generated C function name prefix: `expose('Example')` or `expose('')` for no prefix + - Add `-D dll_link` or `-D static_link` to compile your haxe program into a native library binary + - HaxeCBridge will then generate a header file in your build output directory named after your `--main` class (however a `--main` class is not required to use HaxeCBridge) + - Change the generated library name by adding `-D HaxeCBridge.name=YourLibName` to your hxml + + C-side: + - Include the generated header and link with the hxcpp generated library binary + - Before calling any haxe functions you must start the haxe thread: call `YourLibName_initializeHaxeThread(onHaxeException)` + - Now interact with your haxe library thread by calling the exposed functions + - When your program exits call `YourLibName_stopHaxeThread(true)` + +**/ +#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 HaxeCBridge { + 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 HaxeCBridge { + + 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(['src', '__HaxeCBridgeBindings__.cpp']); + + static final queuedClasses = new Array<{ + cls: Ref<ClassType>, + namespace: String, + }>(); + + // conversion state + static final functionInfo = new Map<String, { + kind: FunctionInfoKind, + hxcppClass: String, + hxcppFunctionName: String, + field: ClassField, + tfunc: TFunc, + rootCTypes: { + args: Array<CType>, + ret: CType + }, + pos: Position, + }>(); + + 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) { + final headerPath = Path.join(['$libName.h']); + + // resolve runtime HaxeCBridge class to make sure it's generated + // add @:buildXml to include generated code + var HaxeCBridgeType = Context.resolveType(macro :HaxeCBridge, Context.currentPos()); + switch HaxeCBridgeType { + case TInst(_.get().meta => meta, params): + if (!meta.has(':buildXml')) { + meta.add(':buildXml', [ + macro $v{code(' + <!-- HaxeCBridge --> + <files id="haxe"> + <file name="$implementationPath"> + <depend name="$headerPath"/> + </file> + </files> + ')} + ], Context.currentPos()); + } + default: throw 'Internal error'; + } + + Context.onAfterTyping(_ -> { + final cConversionContext = new CConverterContext({ + declarationPrefix: libName, + generateTypedef: true, + generateTypedefWithTypeParameters: false, + }); + + for (item in queuedClasses) { + convertQueuedClass(libName, cConversionContext, item.cls, item.namespace); + } + + var header = generateHeader(cConversionContext, libName); + var implementation = generateImplementation(cConversionContext, libName); + + 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, headerPath]), header); + saveFile(Path.join([compilerOutputDir, implementationPath]), implementation); + } + }); + + firstRun = false; + } + + final forloop = fields.slice(0); + for (field in forloop) { + if (field.access.contains(APublic) && !field.access.contains(AOverride) && !field.meta.exists((m) -> m.name == "HaxeCBridge.noemit")) { + switch field.kind { + case FFun(fun): + var wrapper = { + name: field.name + "__fromC", + doc: null, + meta: [{name: "HaxeCBridge.wrapper", params: [], pos: Context.currentPos()}], + access: field.access.filter(a -> a != AAbstract), + kind: null, + pos: Context.currentPos(), + ret: fun.ret + }; + var args = []; + var passArgs = []; + for (arg in fun.args) { + switch arg.type { + case TFunction(taargs, aret): + final aargs = taargs.map(convertSecondaryType); + aret = convertSecondaryType(aret)[0]; + args.push({name: arg.name, type: TPath({name: "Callable", pack: ["cpp"], params: [TPType(TFunction(Lambda.flatten(aargs).concat([TPath({name: "RawPointer", pack: ["cpp"], params: [TPType(TPath({ name: "Void", pack: ["cpp"] }))]})]), aret))]})}); + args.push({name: arg.name + "__context", type: TPath({name: "RawPointer", pack: ["cpp"], params: [TPType(TPath({ name: "Void", pack: ["cpp"] }))]})}); + final lambdaargs = Lambda.flatten(aargs.mapi((i, a) -> + if (a.length < 2) { + [macro $i{"a" + i}]; + } else { + [macro $i{"a" + i}, macro $i{"a" + i}.length]; + } + )).concat([macro $i{arg.name + "__context"}]); + final lambdafargs = aargs.mapi((i, a) -> {name: "a" + i, meta: null, opt: false, type: null, value: null}); + passArgs.push({expr: EFunction(null, { args: lambdafargs, expr: macro return $i{arg.name}($a{lambdaargs}) }), pos: Context.currentPos()}); + default: + passArgs.push(macro $i{arg.name}); + args.push(arg); + } + } + switch (fun.ret) { + case TPath(path) if (path.name == "Array"): + wrapper.ret = TPath({name: "HaxeArray", pack: [], params: path.params.map(t -> convertSecondaryTP(t))}); + default: + } + if (args.length > fun.args.length || fun.ret != wrapper.ret || field.access.contains(AAbstract)) { + wrapper.kind = FFun({ret: wrapper.ret, params: fun.params, expr: macro return $i{field.name}($a{passArgs}), args: args}); + fields.push(wrapper); + field.meta.push({name: "HaxeCBridge.noemit", pos: Context.currentPos()}); + } + case FVar(t, e): + fields.push({ + name: field.name + "__fromC", + doc: null, + meta: [{name: "HaxeCBridge.wrapper", params: [], pos: Context.currentPos()}], + access: field.access, + pos: Context.currentPos(), + kind: FFun({ret: t, params: [], args: [], expr: macro return $i{field.name}}) + }); + default: + } + } + } + + return fields; + } + + static function convertSecondaryTP(tp: TypeParam) { + return switch tp { + case TPType(t): + TPType(convertSecondaryType(t)[0]); + default: + throw "Cannot converty TypeParam: " + tp; + } + } + + static function convertSecondaryType(t: ComplexType) { + return switch TypeTools.follow(Context.resolveType(t, Context.currentPos()), false) { + case TInst(_.get().name => "Array", tps): + [ + TPath({name: "HaxeArray", pack: [], params: tps.map((tp) -> convertSecondaryTP(TPType(Context.toComplexType(tp))))}), + TPath({name: "Int", pack: []}) + ]; + case TInst(_.get().name => "String", _): + [TPath({name: "ConstCharStar", pack: ["cpp"], params: []})]; + case type = TInst(_): + var keyCType = new CConverterContext().tryConvertKeyType(type, false, false, Context.currentPos()); + if (keyCType != null) { + [t]; + } else { + [TPath({name: "HaxeObject", pack: [], params: [TPType(t)]})]; + } + case TAnonymous(_): + Context.error("Cannot expose anonymous struct to C", Context.currentPos()); + default: + [t]; + } + } + + 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 convertQueuedClass(libName: String, cConversionContext: CConverterContext, clsRef: Ref<ClassType>, namespace: String) { + var cls = clsRef.get(); + // validate + if (cls.isInterface) Context.error('Cannot expose interface to C', cls.pos); + if (cls.isExtern) Context.error('Cannot expose extern directly to C', cls.pos); + + // determine the name of the class as generated by hxcpp + var nativeName = getHxcppNativeName(cls); + + var isNativeGen = cls.meta.has(':nativeGen'); + var nativeHxcppName = nativeName + (isNativeGen ? '' : '_obj'); + + // determine the hxcpp generated header path for this class + var typeHeaderPath = nativeName.split('.'); + cConversionContext.requireImplementationHeader(Path.join(typeHeaderPath) + '.h', false); + + // prefix all functions with lib name and class path + var classPrefix = cls.pack.concat([namespace == null ? cls.name : namespace]); + + var cNameMeta = getCNameMeta(cls.meta); + + var functionPrefix = + if (cNameMeta != null) + [cNameMeta]; + else + [libName] + .concat(safeIdent(classPrefix.join('.')) != libName ? classPrefix : []) + .filter(s -> s != ''); + + 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; + } + if (!isConvertibleMethod) return; + + // f is public static function + var fieldExpr = f.expr(); + switch fieldExpr.expr { + case TFunction(tfunc): + // we have to tweak the descriptor for instance constructors and members + var functionDescriptor: TFunc = switch kind { + case Constructor: { + args: tfunc.args, + expr: tfunc.expr, + t: TInst(clsRef, []), // return a instance of this class + } + case Member: + var instanceTArg: TVar = {id: -1, name: 'instance', t: TInst(clsRef, []), meta: null, capture: false, extra: null}; + { + args: [{v: instanceTArg, value: null}].concat(tfunc.args), + expr: tfunc.expr, + t: tfunc.t, + } + case Static: tfunc; + } + + // add C function declaration + var 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 cleanDoc = f.doc != null ? StringTools.trim(removeIndentation(f.doc)) : null; + + cConversionContext.addTypedFunctionDeclaration(cFuncName, functionDescriptor, cleanDoc, f.pos); + + inline function getRootCType(t: Type) { + var tmpCtx = new CConverterContext({generateTypedef: false, generateTypedefForFunctions: false, generateEnums: true}); + return tmpCtx.convertType(t, true, true, f.pos); + } + + var hxcppClass = nativeHxcppName.split('.').join('::'); + + // store useful information about this function that we can use when generating the implementation + functionInfo.set(cFuncName, { + kind: kind, + hxcppClass: nativeName.split('.').join('::'), + hxcppFunctionName: hxcppClass + '::' + switch kind { + case Constructor: '__new'; + case Static | Member: f.name; + }, + field: f, + tfunc: tfunc, + rootCTypes: { + args: functionDescriptor.args.map(a -> getRootCType(a.v.t)), + ret: getRootCType(functionDescriptor.t) + }, + pos: 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.fields.get()) { + convertFunction(f, Member); + } + for (f in cls.statics.get()) { + convertFunction(f, Static); + } + } + + 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 generateHeader(ctx: CConverterContext, namespace: String) { + ctx.requireHeader('stdbool.h', false); // we use bool for _stopHaxeThread() + + var includes = ctx.includes.copy(); + // sort includes, by <, " and alphabetically + includes.sort((a, b) -> { + var i = (a.quoted ? 1 : -1); + var j = (b.quoted ? 1 : -1); + return if (i == j) { + a.path > b.path ? 1 : -1; + } else i - j; + }); + + var prefix = isDynamicLink() ? 'API_PREFIX' : ''; + + return code(' + /** + * $namespace.h + * ${isLibraryBuild() ? + 'Automatically generated by HaxeCBridge' : + '! Warning, binary not generated as a library, make sure to add `-D dll_link` or `-D static_link` when compiling the haxe project !' + } + */ + + #ifndef HaxeCBridge_${namespace}_h + #define HaxeCBridge_${namespace}_h + ') + + (if (includes.length > 0) includes.map(CPrinter.printInclude).join('\n') + '\n\n'; else '') + + (if (ctx.macros.length > 0) ctx.macros.join('\n') + '\n' else '') + + + (if (isDynamicLink()) { + code(' + #ifndef API_PREFIX + #ifdef _WIN32 + #define API_PREFIX __declspec(dllimport) + #else + #define API_PREFIX + #endif + #endif + + '); + } else '') + + + 'typedef void (* HaxeExceptionCallback) (const char* exceptionInfo);\n' + + (if (ctx.supportTypeDeclarations.length > 0) ctx.supportTypeDeclarations.map(d -> CPrinter.printDeclaration(d, true)).join(';\n') + ';\n\n'; else '') + + (if (ctx.typeDeclarations.length > 0) ctx.typeDeclarations.map(d -> CPrinter.printDeclaration(d, true)).join(';\n') + ';\n'; else '') + + + code(' + + #ifdef __cplusplus + extern "C" { + #endif + + /** + * Initializes a haxe thread that executes the haxe main() function remains alive indefinitely until told to stop. + * + * This must be first before calling haxe functions (otherwise those calls will hang waiting for a response from the haxe thread). + * + * @param unhandledExceptionCallback a callback to execute if an unhandled exception occurs on the haxe thread. The haxe thread will continue processing events after an unhandled exception and you may want to stop it after receiving this callback. Use `NULL` for no callback + * @returns `NULL` if the thread initializes successfully or a null-terminated C string if an error occurs during initialization + */ + $prefix const char* ${namespace}_initializeHaxeThread(HaxeExceptionCallback unhandledExceptionCallback); + + /** + * Stops the haxe thread, blocking until the thread has completed. Once ended, it cannot be restarted (this is because static variable state will be retained from the last run). + * + * Other threads spawned from the haxe thread may still be running (you must arrange to stop these yourself for safe app shutdown). + * + * It can be safely called any number of times – if the haxe thread is not running this function will just return. + * + * After executing no more calls to main-thread haxe functions can be made (as these will hang waiting for a response from the main thread). + * + * Thread-safety: Can be called safely called on any thread. If called on the haxe thread it will trigger the thread to stop but it cannot then block until stopped. + * + * @param waitOnScheduledEvents If `true`, this function will wait for all events scheduled to execute in the future on the haxe thread to complete – this is the same behavior as running a normal hxcpp program. If `false`, immediate pending events will be finished and the thread stopped without executing events scheduled in the future + */ + $prefix void ${namespace}_stopHaxeThreadIfRunning(bool waitOnScheduledEvents); + + ') + + indent(1, ctx.supportFunctionDeclarations.map(fn -> CPrinter.printDeclaration(fn, true, prefix)).join(';\n\n') + ';\n\n') + + indent(1, ctx.functionDeclarations.map(fn -> CPrinter.printDeclaration(fn, true, prefix)).join(';\n\n') + ';\n\n') + + + code(' + #ifdef __cplusplus + } + #endif + + #undef API_PREFIX + + #endif /* HaxeCBridge_${namespace}_h */ + '); + } + + static function generateImplementation(ctx: CConverterContext, namespace: String) { + return code(' + /** + * HaxeCBridge Function Binding Implementation + * Automatically generated by HaxeCBridge + */ + #include <hxcpp.h> + #include <hx/Native.h> + #include <hx/Thread.h> + #include <hx/StdLibs.h> + #include <hx/GC.h> + #include <HaxeCBridge.h> + #include <assert.h> + #include <queue> + #include <utility> + #include <atomic> + + // include generated bindings header + ') + + (if (isDynamicLink()) code(' + // set prefix when exporting dll symbols on windows + #ifdef _WIN32 + #define API_PREFIX __declspec(dllexport) + #endif + ') + else + '' + ) + + code(' + #include "../${namespace}.h" + + #define HAXE_C_BRIDGE_LINKAGE HXCPP_EXTERN_CLASS_ATTRIBUTES + ') + + ctx.implementationIncludes.map(CPrinter.printInclude).join('\n') + '\n' + + code(' + + namespace HaxeCBridgeInternal { + + // we cannot use hxcpps HxCreateDetachedThread() because we cannot wait on these threads to end on unix because they are detached threads + #if defined(HX_WINDOWS) + HANDLE haxeThreadNativeHandle = nullptr; + DWORD haxeThreadNativeId = 0; // 0 is not valid thread id + bool createHaxeThread(DWORD (WINAPI *func)(void *), void *param) { + haxeThreadNativeHandle = CreateThread(NULL, 0, func, param, 0, &haxeThreadNativeId); + return haxeThreadNativeHandle != 0; + } + bool waitForThreadExit(HANDLE handle) { + DWORD result = WaitForSingleObject(handle, INFINITE); + return result != WAIT_FAILED; + } + #else + pthread_t haxeThreadNativeHandle; + bool createHaxeThread(void *(*func)(void *), void *param) { + // same as HxCreateDetachedThread(func, param) but without detaching the thread + + pthread_attr_t attr; + if (pthread_attr_init(&attr) != 0) + return false; + if (pthread_create(&haxeThreadNativeHandle, &attr, func, param) != 0 ) + return false; + if (pthread_attr_destroy(&attr) != 0) + return false; + return true; + } + bool waitForThreadExit(pthread_t handle) { + int result = pthread_join(handle, NULL); + return result == 0; + } + #endif + + std::atomic<bool> threadStarted = { false }; + std::atomic<bool> threadRunning = { false }; + // once haxe statics are initialized we cannot clear them for a clean restart + std::atomic<bool> staticsInitialized = { false }; + + struct HaxeThreadData { + HaxeExceptionCallback haxeExceptionCallback; + const char* initExceptionInfo; + }; + + HxSemaphore threadInitSemaphore; + HxMutex threadManageMutex; + + void defaultExceptionHandler(const char* info) { + printf("Unhandled haxe exception: %s\\n", info); + } + + typedef void (* MainThreadCallback)(void* data); + HxMutex queueMutex; + std::queue<std::pair<MainThreadCallback, void*>> queue; + + void runInMainThread(MainThreadCallback callback, void* data) { + queueMutex.Lock(); + queue.push(std::make_pair(callback, data)); + queueMutex.Unlock(); + HaxeCBridge::wakeMainThread(); + } + + // called on the haxe main thread + void processNativeCalls() { + AutoLock lock(queueMutex); + while(!queue.empty()) { + std::pair<MainThreadCallback, void*> pair = queue.front(); + queue.pop(); + pair.first(pair.second); + } + } + + #if defined(HX_WINDOWS) + bool isHaxeMainThread() { + return threadRunning && + (GetCurrentThreadId() == haxeThreadNativeId) && + (haxeThreadNativeId != 0); + } + #else + bool isHaxeMainThread() { + return threadRunning && pthread_equal(haxeThreadNativeHandle, pthread_self()); + } + #endif + } + + THREAD_FUNC_TYPE haxeMainThreadFunc(void *data) { + HX_TOP_OF_STACK + HaxeCBridgeInternal::HaxeThreadData* threadData = (HaxeCBridgeInternal::HaxeThreadData*) data; + + HaxeCBridgeInternal::threadRunning = true; + + threadData->initExceptionInfo = nullptr; + + // copy out callback + HaxeExceptionCallback haxeExceptionCallback = threadData->haxeExceptionCallback; + + bool firstRun = !HaxeCBridgeInternal::staticsInitialized; + + // See hx::Init in StdLibs.cpp for reference + if (!HaxeCBridgeInternal::staticsInitialized) try { + ::hx::Boot(); + __boot_all(); + HaxeCBridgeInternal::staticsInitialized = true; + } catch(Dynamic initException) { + // hxcpp init failure or uncaught haxe runtime exception + threadData->initExceptionInfo = initException->toString().utf8_str(); + } + + if (HaxeCBridgeInternal::staticsInitialized) { // initialized without error + // blocks running the event loop + // keeps alive until manual stop is called + HaxeCBridge::mainThreadInit(HaxeCBridgeInternal::isHaxeMainThread); + HaxeCBridgeInternal::threadInitSemaphore.Set(); + HaxeCBridge::mainThreadRun(HaxeCBridgeInternal::processNativeCalls, haxeExceptionCallback); + } else { + // failed to initialize statics; unlock init semaphore so _initializeHaxeThread can continue and report the exception + HaxeCBridgeInternal::threadInitSemaphore.Set(); + } + + HaxeCBridgeInternal::threadRunning = false; + + THREAD_FUNC_RET + } + + HAXE_C_BRIDGE_LINKAGE + const char* ${namespace}_initializeHaxeThread(HaxeExceptionCallback unhandledExceptionCallback) { + HaxeCBridgeInternal::HaxeThreadData threadData; + threadData.haxeExceptionCallback = unhandledExceptionCallback == nullptr ? HaxeCBridgeInternal::defaultExceptionHandler : unhandledExceptionCallback; + threadData.initExceptionInfo = nullptr; + + { + // mutex prevents two threads calling this function from being able to start two haxe threads + AutoLock lock(HaxeCBridgeInternal::threadManageMutex); + if (!HaxeCBridgeInternal::threadStarted) { + // startup the haxe main thread + HaxeCBridgeInternal::createHaxeThread(haxeMainThreadFunc, &threadData); + + HaxeCBridgeInternal::threadStarted = true; + + // wait until the thread is initialized and ready + HaxeCBridgeInternal::threadInitSemaphore.Wait(); + } else { + threadData.initExceptionInfo = "haxe thread cannot be started twice"; + } + } + + if (threadData.initExceptionInfo != nullptr) { + ${namespace}_stopHaxeThreadIfRunning(false); + + const int returnInfoMax = 1024; + static char returnInfo[returnInfoMax] = ""; // statically allocated for return safety + strncpy(returnInfo, threadData.initExceptionInfo, returnInfoMax); + return returnInfo; + } else { + return nullptr; + } + } + + HAXE_C_BRIDGE_LINKAGE + void ${namespace}_stopHaxeThreadIfRunning(bool waitOnScheduledEvents) { + if (HaxeCBridgeInternal::isHaxeMainThread()) { + // it is possible for stopHaxeThread to be called from within the haxe thread, while another thread is waiting on for the thread to end + // so it is important the haxe thread does not wait on certain locks + HaxeCBridge::endMainThread(waitOnScheduledEvents); + } else { + AutoLock lock(HaxeCBridgeInternal::threadManageMutex); + if (HaxeCBridgeInternal::threadRunning) { + struct Callback { + static void run(void* data) { + bool* b = (bool*) data; + HaxeCBridge::endMainThread(*b); + } + }; + + HaxeCBridgeInternal::runInMainThread(Callback::run, &waitOnScheduledEvents); + + HaxeCBridgeInternal::waitForThreadExit(HaxeCBridgeInternal::haxeThreadNativeHandle); + } + } + } + + HAXE_C_BRIDGE_LINKAGE + void ${namespace}_free(const void* objPtr) { + struct Callback { + static void run(void* data) { + HaxeCBridge::releaseHaxePtr(data); + } + }; + HaxeCBridgeInternal::runInMainThread(Callback::run, (void*)objPtr); + } + ') + + ctx.functionDeclarations.map(d -> generateFunctionImplementation(namespace, d)).join('\n') + '\n' + ; + } + + static function generateFunctionImplementation(namespace: String, d: CDeclaration) { + var signature = switch d.kind {case Function(sig): sig; default: null;}; + var haxeFunction = functionInfo.get(signature.name); + var hasReturnValue = !haxeFunction.rootCTypes.ret.match(Ident('void')); + var externalThread = haxeFunction.field.meta.has('externalThread'); + + // rename signature args to a1, a2, a3 etc, this is to avoid possible conflict with local function variables + var signature: CFunctionSignature = { + name: signature.name, + args: signature.args.mapi((i, arg) -> {name: 'a$i', type: arg.type}), + ret: signature.ret, + } + var d: CDeclaration = { kind: Function(signature) } + + // cast a C type to one which works with hxcpp + inline function castC2Cpp(expr: String, rootCType: CType) { + // type cast argument before passing to hxcpp + return switch rootCType { + case Enum(_): expr; // enum to int works with implicit cast + case Ident('HaxeObject'): 'Dynamic((hx::Object *)$expr)'; // Dynamic cast requires including the hxcpp header of the type + case FunctionPointer(_): 'cpp::Function<${CPrinter.printType(rootCType, "")}>($expr)'; + case Ident(_), InlineStruct(_), Pointer(_): expr; // hxcpp auto casting works + } + } + + inline function castCpp2C(expr: String, cType: CType, rootCType: CType) { + // cast hxcpp type to c + return switch rootCType { + case Enum(_): 'static_cast<${CPrinter.printType(cType)}>($expr)'; // need explicit cast for int -> enum + case Ident('HaxeObject'): 'HaxeCBridge::retainHaxeObject($expr)'; // Dynamic cast requires including the hxcpp header of the type + case Ident('HaxeString'): 'HaxeCBridge::retainHaxeString($expr)'; // ensure string is held by the GC (until manual release) + case Ident(_), FunctionPointer(_), InlineStruct(_), Pointer(_): expr; // hxcpp auto casting works + } + } + + inline function callWithArgs(argNames: Array<String>) { + var callExpr = switch haxeFunction.kind { + case Constructor | Static: + '${haxeFunction.hxcppFunctionName}(${argNames.mapi((i, arg) -> castC2Cpp(arg, haxeFunction.rootCTypes.args[i])).join(', ')})'; + case Member: + var a0Name = argNames[0]; + var argNames = argNames.slice(1); + var argCTypes = haxeFunction.rootCTypes.args.slice(1); + '(${haxeFunction.hxcppClass}((hx::Object *)$a0Name, true))->${haxeFunction.field.name}(${argNames.mapi((i, arg) -> castC2Cpp(arg, argCTypes[i])).join(', ')})'; + } + + return if (hasReturnValue) { + castCpp2C(callExpr, signature.ret, haxeFunction.rootCTypes.ret); + } else { + callExpr; + } + } + + if (externalThread) { + // straight call through + return ( + code(' + HAXE_C_BRIDGE_LINKAGE + ${CPrinter.printDeclaration(d, false)} { + hx::NativeAttach autoAttach; + return ${callWithArgs(signature.args.map(a->a.name))}; + } + ') + ); + } else { + // main thread synchronization implementation + var fnDataTypeName = 'Data'; + var fnDataName = 'data'; + var fnDataStruct: CStruct = { + fields: [ + { + name: 'args', + type: InlineStruct({fields: signature.args}) + }, + { + name: 'lock', + type: Ident('HxSemaphore') + } + ].concat( + hasReturnValue ? [{ + name: 'ret', + type: signature.ret + }] : [] + ) + }; + + var fnDataDeclaration: CDeclaration = { kind: Struct(fnDataTypeName, fnDataStruct) } + + var argCTypes = haxeFunction.rootCTypes.args; + return ( + code(' + HAXE_C_BRIDGE_LINKAGE + ') + + CPrinter.printDeclaration(d, false) + ' {\n' + + indent(1, + code(' + if (HaxeCBridgeInternal::isHaxeMainThread()) { + return ${callWithArgs(signature.args.map(a->a.name))}; + } + ') + + CPrinter.printDeclaration(fnDataDeclaration) + ';\n' + + code(' + struct Callback { + static void run(void* p) { + // executed within the haxe main thread + $fnDataTypeName* $fnDataName = ($fnDataTypeName*) p; + try { + ${hasReturnValue ? + '$fnDataName->ret = ${callWithArgs(signature.args.map(a->'$fnDataName->args.${a.name}'))};' : + '${callWithArgs(signature.args.map(a->'$fnDataName->args.${a.name}'))};' + } + $fnDataName->lock.Set(); + } catch(Dynamic runtimeException) { + $fnDataName->lock.Set(); + throw runtimeException; + } + } + }; + + #ifdef HXCPP_DEBUG + assert(HaxeCBridgeInternal::threadRunning && "haxe thread not running, use ${namespace}_initializeHaxeThread() to activate the haxe thread"); + #endif + + $fnDataTypeName $fnDataName = { {${signature.args.map(a->a.name).join(', ')}} }; + + // queue a callback to execute ${haxeFunction.field.name}() on the main thread and wait until execution completes + HaxeCBridgeInternal::runInMainThread(Callback::run, &$fnDataName); + $fnDataName.lock.Wait(); + ') + + if (hasReturnValue) code(' + return $fnDataName.ret; + ') else '' + ) + + code(' + } + ') + ); + } + } + + /** + 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 FunctionInfoKind { + Constructor; + Member; + Static; +} + +enum CModifier { + Const; +} + +enum CType { + Ident(name: String, ?modifiers: Array<CModifier>); + Pointer(t: CType, ?modifiers: Array<CModifier>); + FunctionPointer(name: String, argTypes: Array<CType>, ret: CType, ?modifiers: Array<CModifier>); + InlineStruct(struct: CStruct); + Enum(name: String); +} + +// not exactly specification C but good enough for this purpose +enum CDeclarationKind { + Typedef(type: CType, declarators: Array<String>); + Enum(name: String, fields: Array<{name: String, ?value: Int}>); + Function(fun: CFunctionSignature); + Struct(name: String, struct: CStruct); + Variable(name: String, type: CType); +} + +enum CCustomMeta { + CppFunction(str: String); +} + +typedef CDeclaration = { + kind: CDeclarationKind, + ?doc: String, +} + +typedef CStruct = { + fields: Array<{name: String, type: CType}> +} + +typedef CFunctionSignature = { + name: String, + args: Array<{name: String, type: CType}>, + ret: CType +} + +typedef CInclude = { + path: String, + quoted: Bool, +} + +typedef CMacro = { + directive: String, + name: String, + content: String, +} + +class CPrinter { + + public static function printInclude(inc: CInclude) { + return '#include ${inc.quoted ? '"' : '<'}${inc.path}${inc.quoted ? '"' : '>'}'; + } + + public static function printMacro(cMacro: CMacro) { + var escapedContent = cMacro.content.replace('\n', '\n\\'); + return '#${cMacro.directive} ${cMacro.name} ${escapedContent}'; + } + + public static function printType(cType: CType, argName: Null<String> = null): String { + return switch cType { + case Ident(name, modifiers): + (hasModifiers(modifiers) ? (printModifiers(modifiers) + ' ') : '') + name + (argName == null ? '' : ' $argName'); + case Pointer(t, modifiers): + printType(t) + '*' + (hasModifiers(modifiers) ? (' ' + printModifiers(modifiers)) : '') + (argName == null ? '' : ' $argName'); + case FunctionPointer(name, argTypes, ret): + if (argName == "") { + '${printType(ret)}(${argTypes.length > 0 ? argTypes.map((t) -> printType(t)).join(', ') : 'void'})'; + } else if (argName == null) { + '${printType(ret)} (* $name) (${argTypes.length > 0 ? argTypes.map((t) -> printType(t)).join(', ') : 'void'})'; + } else { + '${printType(ret)} (* $argName) (${argTypes.length > 0 ? argTypes.map((t) -> printType(t)).join(', ') : 'void'})'; + } + case InlineStruct(struct): + 'struct {${printFields(struct.fields, false)}}' + (argName == null ? '' : ' $argName'); + case Enum(name): + 'enum $name' + (argName == null ? '' : ' $argName'); + } + } + + public static function printDeclaration(cDeclaration: CDeclaration, docComment: Bool = true, qualifier: String = '') { + return + (cDeclaration.doc != null && docComment ? (printDoc(cDeclaration.doc) + '\n') : '') + + (qualifier != '' ? (qualifier + ' ') : '') + + switch cDeclaration.kind { + case Typedef(type, declarators): + 'typedef ${printType(type)}' + (declarators.length > 0 ? ' ${declarators.join(', ')}' :''); + case Enum(name, fields): + 'enum $name {\n' + + fields.map(f -> '\t' + f.name + (f.value != null ? ' = ${f.value}' : '')).join(',\n') + '\n' + + '}'; + case Struct(name, {fields: fields}): + 'struct $name {\n' + + printFields(fields, true) + + '}'; + case Function(sig): + printFunctionSignature(sig); + case Variable(name, type): + '${printType} $name'; + } + } + + public static function printFields(fields: Array<{name: String, type: CType}>, newlines: Bool) { + var sep = (newlines?'\n':' '); + return fields.map(f -> '${newlines?'\t':''}${printField(f)}').join(sep) + (newlines?'\n':''); + } + + public static function printField(f: {name: String, type: CType}) { + return '${printType(f.type, f.name)};'; + } + + public static function printFunctionSignature(signature: CFunctionSignature) { + var name = signature.name; + var args = signature.args; + var ret = signature.ret; + return '${printType(ret)} $name(${args.map(arg -> '${printType(arg.type, arg.name)}').join(', ')})'; + } + + public static function printDoc(doc: String) { + return '/**\n${doc.split('\n').map(l -> ' * ' + l).join('\n')}\n */'; + } + + static function hasModifiers(modifiers: Null<Array<CModifier>>) + return modifiers != null && modifiers.length > 0; + + public static function printModifiers(modifiers: Null<Array<CModifier>>) { + return if (hasModifiers(modifiers)) modifiers.map(printModifier).join('\n'); + else ''; + } + + public static function printModifier(modifier: CModifier) { + return switch modifier { + case Const: 'const'; + } + } + +} + +class CConverterContext { + + public final includes = new Array<CInclude>(); + public final implementationIncludes = new Array<CInclude>(); + public final macros = new Array<String>(); + + public final supportTypeDeclarations = new Array<CDeclaration>(); + final supportDeclaredTypeIdentifiers = new Map<String, Bool>(); + + public final supportFunctionDeclarations = new Array<CDeclaration>(); + final supportDeclaredFunctionIdentifiers = new Map<String, Position>(); + + public final typeDeclarations = new Array<CDeclaration>(); + final declaredTypeIdentifiers = new Map<String, Bool>(); + + public final functionDeclarations = new Array<CDeclaration>(); + final declaredFunctionIdentifiers = new Map<String, Position>(); + + final declarationPrefix: String; + final generateTypedef: Bool; + final generateTypedefForFunctions: Bool; + final generateTypedefWithTypeParameters: Bool; + final generateEnums: Bool; + + /** + namespace is used to prefix types + **/ + public function new(options: { + ?declarationPrefix: String, + ?generateTypedef: Bool, + ?generateTypedefForFunctions: Bool, + /** type parameter name is appended to the typedef ident, this makes for long type names so it's disabled by default **/ + ?generateTypedefWithTypeParameters: Bool, + ?generateEnums: Bool, + } = null) { + this.declarationPrefix = (options != null && options.declarationPrefix != null) ? options.declarationPrefix : ''; + this.generateTypedef = (options != null && options.generateTypedef != null) ? options.generateTypedef : true; + this.generateTypedefForFunctions = (options != null && options.generateTypedefForFunctions != null) ? options.generateTypedefForFunctions : true; + this.generateTypedefWithTypeParameters = (options != null && options.generateTypedefWithTypeParameters != null) ? options.generateTypedefWithTypeParameters : false; + this.generateEnums = (options != null && options.generateEnums != null) ? options.generateEnums : true; + } + + public function addFunctionDeclaration(name: String, fun: Function, doc: Null<String>, pos: Position) { + functionDeclarations.push({ + doc: doc, + kind: Function({ + name: name, + args: fun.args.map(arg -> { + name: cKeywords.has(arg.name) ? (arg.name + '_') : arg.name, + type: convertComplexType(arg.type, true, pos) + }), + ret: convertComplexType(fun.ret, true, pos) + }) + }); + declareFunctionIdentifier(name, pos); + } + + public function addTypedFunctionDeclaration(name: String, tfunc: TFunc, doc: Null<String>, pos: Position) { + functionDeclarations.push({ + doc: doc, + kind: Function({ + name: name, + args: tfunc.args.map(arg -> { + name: cKeywords.has(arg.v.name) ? (arg.v.name + '_') : arg.v.name, + type: convertType(arg.v.t, true, false, pos) + }), + ret: convertType(tfunc.t, true, false, pos) + }) + }); + declareFunctionIdentifier(name, pos); + } + + function declareFunctionIdentifier(name: String, pos: Position) { + var existingDecl = declaredFunctionIdentifiers.get(name); + if (existingDecl == null) { + declaredFunctionIdentifiers.set(name, pos); + } else { + inline function locString(p: Position) { + var l = PositionTools.toLocation(p); + return '${l.file}:${l.range.start.line}'; + } + Context.fatalError('HaxeCBridge: function "$name" (${locString(pos)}) generates the same C name as another function (${locString(existingDecl)})', pos); + } + } + + public function convertComplexType(ct: ComplexType, allowNonTrivial: Bool, pos: Position) { + return convertType(Context.resolveType(ct, pos), allowNonTrivial, false, pos); + } + + public function convertType(type: Type, allowNonTrivial: Bool, allowBareFnTypes: Bool, pos: Position): CType { + var hasCoreTypeIndication = { + var baseType = asBaseType(type); + if (baseType != null) { + var t = baseType.t; + // externs in the cpp package are expected to be key-types + t.isExtern && (t.pack[0] == 'cpp') || + t.meta.has(":coreType") || + // hxcpp doesn't mark its types as :coreType but uses :semantics and :noPackageRestrict sometimes + t.meta.has(":semantics") || + t.meta.has(":noPackageRestrict"); + } else false; + } + + if (hasCoreTypeIndication) { + return convertKeyType(type, allowNonTrivial, allowBareFnTypes, pos); + } + + return switch type { + case TInst(_.get() => t, params): + var keyCType = tryConvertKeyType(type, allowNonTrivial, allowBareFnTypes, pos); + if (keyCType != null) { + keyCType; + } else if (t.isExtern) { + // we can expose extern types (assumes they're compatible with C) + var ident = { + var nativeMeta = t.meta.extract(':native')[0]; + var nativeMetaValue = switch nativeMeta { + case null: null; + case {params: [{expr: EConst(CString(value))}]}: value; + default: null; + } + nativeMetaValue != null ? nativeMetaValue : t.name; + } + // if the extern has @:include metas, copy the referenced header files so we can #include them locally + var includes = t.meta.extract(':include'); + for (include in includes) { + switch include.params { + case null: + case [{expr: EConst(CString(includePath))}]: + // copy the referenced include into the compiler output directory and require this header + var filename = Path.withoutDirectory(includePath); + var absoluteIncludePath = Path.join([getAbsolutePosDirectory(t.pos), includePath]); + var targetDirectory = Compiler.getOutput(); + var targetFilePath = Path.join([targetDirectory, filename]); + + if (!FileSystem.exists(targetDirectory)) { + // creates intermediate directories if required + FileSystem.createDirectory(targetDirectory); + } + + File.copy(absoluteIncludePath, targetFilePath); + requireHeader(filename, true); + default: + } + } + Ident(ident); + } else { + if (allowNonTrivial) { + // return an opaque pointer to this object + // the implementation must include the hxcpp header associated with this type for dynamic casting to work + var nativeName = @:privateAccess HaxeCBridge.getHxcppNativeName(t); + var hxcppHeaderPath = Path.join(nativeName.split('.')) + '.h'; + requireImplementationHeader(hxcppHeaderPath, false); + // 'HaxeObject' c typedef + getHaxeObjectCType(type); + } else { + Context.error('Type ${TypeTools.toString(type)} is not supported as secondary type for C export, use HaxeCBridge.HaxeObject<${TypeTools.toString(type)}> instead', pos); + } + } + + case TFun(args, ret): + if (allowBareFnTypes) { + getFunctionCType(args, ret, pos); + } else { + Context.error("Callbacks must be wrapped in cpp.Callable<T> when exposing to C", pos); + } + + case TAnonymous(a): + if (allowNonTrivial) { + getHaxeObjectCType(type); + } else { + Context.error('Structures are not supported as secondary type for C export, use HaxeCBridge.HaxeObject<T> instead', pos); + } + + case TAbstract(_.get() => t, _): + var keyCType = tryConvertKeyType(type, allowNonTrivial, allowBareFnTypes, pos); + if (keyCType != null) { + keyCType; + } else { + var isPublicEnumAbstract = t.meta.has(':enum') && !t.isPrivate; + var isIntEnumAbstract = if (isPublicEnumAbstract) { + var underlyingRootType = TypeTools.followWithAbstracts(t.type, false); + Context.unify(underlyingRootType, Context.resolveType(macro :Int, Context.currentPos())); + } else false; + if (isIntEnumAbstract && generateEnums) { + // c-enums can be converted to ints + getEnumCType(type, allowNonTrivial, pos); + } else { + // follow once abstract's underling type + + // check if the abstract is wrapping a key type + var underlyingKeyType = tryConvertKeyType(t.type, allowNonTrivial, allowBareFnTypes, pos); + if (underlyingKeyType != null) { + underlyingKeyType; + } else { + // we cannot use t.type here because we need to account for haxe special abstract resolution behavior like multiType with Map + convertType(TypeTools.followWithAbstracts(type, true), allowNonTrivial, allowBareFnTypes, pos); + } + } + } + + case TType(_.get() => t, params): + var keyCType = tryConvertKeyType(type, allowNonTrivial, allowBareFnTypes, pos); + if (keyCType != null) { + keyCType; + } else { + + var useDeclaration = + generateTypedef && + (params.length > 0 ? generateTypedefWithTypeParameters : true) && + !t.isPrivate; + + if (useDeclaration) { + getTypeAliasCType(type, allowNonTrivial, allowBareFnTypes, pos); + } else { + // follow type alias (with type parameter) + convertType(TypeTools.follow(type, true), allowNonTrivial, allowBareFnTypes, pos); + } + } + + case TLazy(f): + convertType(f(), allowNonTrivial, allowBareFnTypes, pos); + + case TDynamic(t): + if (allowNonTrivial) { + getHaxeObjectCType(type); + } else { + Context.error('Any and Dynamic are not supported as secondary type for C export, use HaxeCBridge.HaxeObject<Any> instead', pos); + } + + case TMono(t): + Context.error("Explicit type is required when exposing to C", pos); + + case TEnum(t, params): + Context.error("Exposing enum types to C is not supported, try using an enum abstract over Int", pos); + } + } + + /** + Convert a key type and expect a result (or fail) + A key type is like a core type (and includes :coreType types) but also includes hxcpp's own special types that don't have the :coreType annotation + **/ + function convertKeyType(type: Type, allowNonTrivial:Bool, allowBareFnTypes: Bool, pos: Position): CType { + var keyCType = tryConvertKeyType(type, allowNonTrivial, allowBareFnTypes, pos); + return if (keyCType == null) { + var p = new Printer(); + Context.warning('No corresponding C type found for "${TypeTools.toString(type)}" (using void* instead)', pos); + Pointer(Ident('void')); + } else keyCType; + } + + /** + Return CType if Type was a key type and null otherwise + **/ + public function tryConvertKeyType(type: Type, allowNonTrivial:Bool, allowBareFnTypes: Bool, pos: Position): Null<CType> { + var base = asBaseType(type); + return if (base != null) { + switch base { + + /** + See `cpp_type_of` in gencpp.ml + https://github.com/HaxeFoundation/haxe/blob/65bb88834cea059035a73db48e79c7a5c5817ee8/src/generators/gencpp.ml#L1743 + **/ + + case {t: {pack: [], name: "Null"}, params: [tp]}: + // Null<T> isn't supported, so we convert T instead + convertType(tp, allowNonTrivial, allowBareFnTypes, pos); + + case {t: {pack: [], name: "Array"}}: + Context.error("Array<T> is not supported for C export, try using cpp.Pointer<T> instead", pos); + + case {t: {pack: [], name: 'Void' | 'void'}}: Ident('void'); + case {t: {pack: [], name: "Bool"}}: requireHeader('stdbool.h'); Ident("bool"); + case {t: {pack: [], name: "Float"}}: Ident("double"); + case {t: {pack: [], name: "Int"}}: Ident("int"); + case {t: {pack: [], name: "Single"}}: Ident("float"); + + case {t: {pack: ["cpp"], name: "Void"}}: Ident('void'); + case {t: {pack: ["cpp"], name: "SizeT"}}: requireHeader('stddef.h'); Ident("size_t"); + case {t: {pack: ["cpp"], name: "Char"}}: Ident("char"); + case {t: {pack: ["cpp"], name: "Float32"}}: Ident("float"); + case {t: {pack: ["cpp"], name: "Float64"}}: Ident("double"); + case {t: {pack: ["cpp"], name: "Int8"}}: Ident("signed char"); + case {t: {pack: ["cpp"], name: "Int16"}}: Ident("short"); + case {t: {pack: ["cpp"], name: "Int32"}}: Ident("int"); + case {t: {pack: ["cpp"], name: "Int64"}}: requireHeader('stdint.h'); Ident("int64_t"); + case {t: {pack: ["cpp"], name: "UInt8"}}: Ident("unsigned char"); + case {t: {pack: ["cpp"], name: "UInt16"}}: Ident("unsigned short"); + case {t: {pack: ["cpp"], name: "UInt32"}}: Ident("unsigned int"); + case {t: {pack: ["cpp"], name: "UInt64"}}: requireHeader('stdint.h'); Ident("uint64_t"); + + case {t: {pack: ["cpp"], name: "Star" | "RawPointer"}, params: [tp]}: Pointer(convertType(tp, false, allowBareFnTypes, pos)); + case {t: {pack: ["cpp"], name: "ConstStar" | "RawConstPointer" }, params: [tp]}: Pointer(setModifier(convertType(tp, false, allowBareFnTypes, pos), Const)); + + // non-trivial types + // hxcpp will convert these automatically if primary type but not if secondary (like as argument type or pointer type) + case {t: {pack: [], name: "String"}}: + if (allowNonTrivial) { + getHaxeStringCType(type); + } else { + Context.error('String is not supported as secondary type for C export, use cpp.ConstCharStar instead', pos); + } + + case {t: {pack: ["cpp"], name: "Pointer"}, params: [tp]}: + if (allowNonTrivial) { + Pointer(convertType(tp, false, allowBareFnTypes, pos)); + } else { + Context.error('cpp.Pointer is not supported as secondary type for C export, use cpp.Star or cpp.RawPointer instead', pos); + } + case {t: {pack: ["cpp"], name: "ConstPointer" }, params: [tp]}: + if (allowNonTrivial) { + Pointer(setModifier(convertType(tp, false, allowBareFnTypes, pos), Const)); + } else { + Context.error('cpp.ConstPointer is not supported as secondary type for C export, use cpp.ConstStar or cpp.RawRawPointer instead', pos); + } + case {t: {pack: ["cpp"], name: "Callable" | "CallableData"}, params: [tp]}: + if (allowNonTrivial) { + convertType(tp, false, true, pos); + } else { + Context.error('${base.t.pack.concat([base.t.name]).join('.')} is not supported as secondary type for C export', pos); + } + case {t: {pack: ["cpp"], name: "Function"}, params: [tp, abi]}: + if (allowNonTrivial) { + convertType(tp, false, true, pos); + } else { + Context.error('${base.t.pack.concat([base.t.name]).join('.')} is not supported as secondary type for C export', pos); + } + + case {t: {pack: ["cpp"], name: name = + "Reference" | + "AutoCast" | + "VarArg" | + "FastIterator" + }}: + Context.error('cpp.$name is not supported for C export', pos); + + default: + // case {pack: [], name: "EnumValue"}: Ident + // case {pack: [], name: "Class"}: Ident + // case {pack: [], name: "Enum"}: Ident + // case {pack: ["cpp"], name: "Object"}: Ident; + // (* Things with type parameters hxcpp knows about ... *) + // | (["cpp"],"Struct"), [param] -> + // TCppStruct(cpp_type_of stack ctx param) + null; + } + } else null; + } + + function asBaseType(type: Type): Null<{t: BaseType, params: Array<Type>}> { + return switch type { + case TMono(t): null; + case TEnum(t, params): {t: t.get(), params: params}; + case TInst(t, params): {t: t.get(), params: params}; + case TType(t, params): {t: t.get(), params: params}; + case TAbstract(t, params): {t: t.get(), params: params}; + case TFun(args, ret): null; + case TAnonymous(a): null; + case TDynamic(t): null; + case TLazy(f): asBaseType(f()); + } + } + + function setModifier(cType: CType, modifier: CModifier): CType { + inline function _setModifier(modifiers: Null<Array<CModifier>>) { + return if (modifiers == null) { + [modifier]; + } else if (!modifiers.has(modifier)) { + modifiers.push(modifier); + modifiers; + } else modifiers; + } + return switch cType { + case Ident(name, modifiers): Ident(name, _setModifier(modifiers)); + case Pointer(type, modifiers): Pointer(type, _setModifier(modifiers)); + case FunctionPointer(name, argTypes, ret, modifiers): FunctionPointer(name, argTypes, ret, _setModifier(modifiers)); + case InlineStruct(struct): cType; + case Enum(name): cType; + } + } + + public function requireHeader(path: String, quoted: Bool = false) { + if (!includes.exists(f -> f.path == path && f.quoted == quoted)) { + includes.push({ + path: path, + quoted: quoted + }); + } + } + + public function requireImplementationHeader(path: String, quoted: Bool = false) { + if (!implementationIncludes.exists(f -> f.path == path && f.quoted == quoted)) { + implementationIncludes.push({ + path: path, + quoted: quoted + }); + } + } + + function getEnumCType(type: Type, allowNonTrivial: Bool, pos: Position): CType { + var ident = safeIdent(declarationPrefix + '_' + typeDeclarationIdent(type, false)); + + // `enum ident` is considered non-trivial + if (!allowNonTrivial) { + Context.error('Enums are not allowed as secondary types, consider using Int instead', pos); + } + + if (!declaredTypeIdentifiers.exists(ident)) { + + switch type { + case TAbstract(_.get() => a, params) if(a.meta.has(':enum')): + var enumFields = a.impl.get().statics.get() + .filter(field -> field.meta.has(':enum') && field.meta.has(':value')) + .map(field -> { + name: safeIdent(field.name), + value: getValue(field.meta.extract(':value')[0].params[0]) + }); + + typeDeclarations.push({kind: Enum(ident, enumFields)}); + + default: Context.fatalError('Internal error: Expected enum abstract but got $type', pos); + } + declaredTypeIdentifiers.set(ident, true); + + } + return Enum(ident); + } + + function getTypeAliasCType(type: Type, allowNonTrivial: Bool, allowBareFnTypes: Bool, pos: Position): CType { + var ident = safeIdent(declarationPrefix + '_' + typeDeclarationIdent(type, false)); + + // order of typedef typeDeclarations should be dependency correct because required typedefs are added before this typedef is added + // we call this outside the exists() branch below to make sure `allowNonTrivial` and `allowBareFnTypes` errors will be caught + // otherwise the following may allow a non-trivial type as a secondary: + // func(a: NonTrivialAlias, b: Star<NonTrivialAlias>) because `NonTrivialAlias` is first created when converting `a` and then referenced without checks for `b` + var aliasedType = convertType(TypeTools.follow(type, true), allowNonTrivial, allowBareFnTypes, pos); + + if (!declaredTypeIdentifiers.exists(ident)) { + + typeDeclarations.push({kind: Typedef(aliasedType, [ident])}); + declaredTypeIdentifiers.set(ident, true); + + } + return Ident(ident); + } + + function getFunctionCType(args: Array<{name: String, opt: Bool, t: Type}>, ret: Type, pos: Position): CType { + // optional type parameters are not supported and become non-optional + + var ident = safeIdent('function_' + args.map(arg -> typeDeclarationIdent(arg.t, false)).concat([typeDeclarationIdent(ret, false)]).join('_')); + var funcPointer: CType = FunctionPointer( + ident, + args.map(arg -> convertType(arg.t, false, false, pos)), + convertType(ret, false, false, pos) + ); + + if (false && generateTypedefForFunctions) { + if (!declaredTypeIdentifiers.exists(ident)) { + typeDeclarations.push({kind: Typedef(funcPointer, []) }); + declaredTypeIdentifiers.set(ident, true); + } + return Ident(ident); + } else { + return funcPointer; + } + } + + function getHaxeObjectCType(t: Type): CType { + // in the future we could specialize based on t (i.e. generating another typedef name like HaxeObject_SomeType) + var typeIdent = 'HaxeObject'; + var functionIdent = '${declarationPrefix}_free'; + + if (!supportDeclaredTypeIdentifiers.exists(typeIdent)) { + supportTypeDeclarations.push({ + kind: Typedef(Pointer(Ident('void')), [typeIdent]), + doc: code(' + Represents a pointer to a haxe object. + When passed from haxe to C, a reference to the object is retained to prevent garbage collection. You should call ${functionIdent} when finished with this handle in C to allow collection.') + }); + supportDeclaredTypeIdentifiers.set(typeIdent, true); + } + + if (!supportDeclaredFunctionIdentifiers.exists(functionIdent)) { + supportFunctionDeclarations.push({ + doc: code(' + Informs the garbage collector that object is no longer needed by the C code. + + If the object has no remaining reference the garbage collector can free the associated memory (which can happen at any time in the future). It does not free the memory immediately. + + Thread-safety: can be called on any thread. + + @param haxeObject a handle to an arbitrary haxe object returned from a haxe function'), + kind: Function({ + name: functionIdent, + args: [{name: 'haxeObject', type: Ident("const void*")}], + ret: Ident('void') + }) + }); + supportDeclaredFunctionIdentifiers.set(functionIdent, Context.currentPos()); + } + + return Ident(typeIdent); + } + + function getHaxeStringCType(t: Type): CType { + getHaxeObjectCType(null); // Make sure free is set up + // in the future we could specialize based on t (i.e. generating another typedef name like HaxeObject_SomeType) + var typeIdent = 'HaxeString'; + var functionIdent = '${declarationPrefix}_free'; + + if (!supportDeclaredTypeIdentifiers.exists(typeIdent)) { + supportTypeDeclarations.push({ + kind: Typedef(Pointer(Ident("char", [Const])), [typeIdent]), + doc: code(' + Internally haxe strings are stored as null-terminated C strings. Cast to char16_t if you expect utf16 strings. + When passed from haxe to C, a reference to the object is retained to prevent garbage collection. You should call ${functionIdent} when finished with this handle to allow collection.') + }); + supportDeclaredTypeIdentifiers.set(typeIdent, true); + } + + if (false && !supportDeclaredFunctionIdentifiers.exists(functionIdent)) { + supportFunctionDeclarations.push({ + doc: code(' + Informs the garbage collector that the string is no longer needed by the C code. + + If the object has no remaining reference the garbage collector can free the associated memory (which can happen at any time in the future). It does not free the memory immediately. + + Thread-safety: can be called on any thread. + + @param haxeString a handle to a haxe string returned from a haxe function'), + kind: Function({ + name: functionIdent, + args: [{name: 'haxeString', type: Ident(typeIdent)}], + ret: Ident('void') + }) + }); + supportDeclaredFunctionIdentifiers.set(functionIdent, Context.currentPos()); + } + + return Ident(typeIdent); + } + + // generate a type identifier for declaring a haxe type in C + function typeDeclarationIdent(type: Type, useSafeIdent: Bool) { + var s = TypeTools.toString(type); + return useSafeIdent ? safeIdent(s) : s; + } + + 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; + if (cKeywords.has(str)) { + str = str + '_'; + } + return str; + } + + /** + Return the directory of the Context's current position + + For a @:build macro, this is the directory of the haxe file it's added to + **/ + static function getAbsolutePosDirectory(pos: haxe.macro.Expr.Position) { + var classPosInfo = Context.getPosInfos(pos); + var classFilePath = Path.isAbsolute(classPosInfo.file) ? classPosInfo.file : Path.join([Sys.getCwd(), classPosInfo.file]); + return Path.directory(classFilePath); + } + + /** + Extends ExprTools.getValue to skip through ECast + **/ + static function getValue(expr: Expr) { + return switch expr.expr { + case ECast(e, t): getValue(e); + default: ExprTools.getValue(expr); + } + } + + static public final cKeywords: Array<String> = [ + "auto", "double", "int", "struct", "break", "else", "long", "switch", "case", "enum", "register", "typedef", "char", "extern", "return", "union", "const", "float", "short", "unsigned", "continue", "for", "signed", "void", "default", "goto", "sizeof", "volatile", "do", "if", "static", "while", + "size_t", "int64_t", "uint64_t", + // HaxeCBridge types + "HaxeObject", "HaxeExceptionCallback", + // hxcpp + "Int", "String", "Float", "Dynamic", "Bool", + ]; + +} + +class CodeTools { + + static public function code(str: String) { + str = ~/^[ \t]*\n/.replace(str, ''); + str = ~/\n[ \t]*$/.replace(str, '\n'); + return removeIndentation(str); + } + + /** + Remove common indentation from lines in a string + **/ + static public function removeIndentation(str: String) { + // find common indentation across all lines + var lines = str.split('\n'); + var commonTabsCount: Null<Int> = null; + var commonSpaceCount: Null<Int> = null; + var spacePrefixPattern = ~/^([ \t]*)[^\s]/; + for (line in lines) { + if (spacePrefixPattern.match(line)) { + var space = spacePrefixPattern.matched(1); + var tabsCount = 0; + var spaceCount = 0; + for (i in 0...space.length) { + if (space.charAt(i) == '\t') tabsCount++; + if (space.charAt(i) == ' ') spaceCount++; + } + commonTabsCount = commonTabsCount != null ? Std.int(Math.min(commonTabsCount, tabsCount)) : tabsCount; + commonSpaceCount = commonSpaceCount != null ? Std.int(Math.min(commonSpaceCount, spaceCount)) : spaceCount; + } + } + + var spaceCharCount: Int = commonTabsCount + commonSpaceCount; + + // remove commonSpacePrefix from lines + return spaceCharCount > 0 ? lines.map( + line -> spacePrefixPattern.match(line) ? line.substr(spaceCharCount) : line + ).join('\n') : str; + } + + static public function indent(level: Int, str: String) { + var t = [for (i in 0...level) '\t'].join(''); + str = str.split('\n').map(l -> { + if (~/^[ \t]*$/.match(l)) l else t + l; + }).join('\n'); + return str; + } + +} + + #end // (display || display_details || target.name != cpp) + +#elseif (cpp && !cppia) +// runtime HaxeCBridge + +import cpp.Callable; +import cpp.Int64; +import cpp.Star; +import haxe.EntryPoint; +import sys.thread.Lock; +import sys.thread.Mutex; +import sys.thread.Thread; + +abstract HaxeObject<T>(cpp.RawPointer<cpp.Void>) from cpp.RawPointer<cpp.Void> to cpp.RawPointer<cpp.Void> { + public var value(get, never): T; + + @:to + public inline function toDynamic(): Dynamic { + return untyped __cpp__('Dynamic((hx::Object *){0})', this); + } + + @:to + inline function get_value(): T { + return toDynamic(); + } + + @:from + public static inline function fromT<T>(x: T): HaxeObject<T> { + return cast cpp.Pointer.addressOf(x); + } +} + +abstract HaxeArray<T>(cpp.RawPointer<cpp.RawPointer<cpp.Void>>) from cpp.RawPointer<cpp.RawPointer<cpp.Void>> to cpp.RawPointer<cpp.RawPointer<cpp.Void>> { + @:from + public static inline function fromArrayT<T>(x: Array<T>): HaxeArray<HaxeObject<T>> { + return HaxeCBridge.retainHaxeArray(x); + } +} + +@:nativeGen +@:keep +@:noCompletion +class HaxeCBridge { + + #if (haxe_ver >= 4.2) + @:noCompletion + static public function mainThreadInit(isMainThreadCb: cpp.Callable<Void -> Bool>) @:privateAccess { + // replaces __hxcpp_main() in __main__.cpp + #if (haxe_ver < 4.201) + Thread.initEventLoop(); + #end + + Internal.isMainThreadCb = isMainThreadCb; + Internal.mainThreadWaitLock = Thread.current().events.waitLock; + + #if (haxe_ver < 4.201) + EntryPoint.init(); + #end + } + + @:noCompletion + static public function mainThreadRun(processNativeCalls: cpp.Callable<Void -> Void>, onUnhandledException: cpp.Callable<cpp.ConstCharStar -> Void>) @:privateAccess { + try { + runUserMain(); + } catch (e: Any) { + onUnhandledException(Std.string(e)); + } + + // run always-alive event loop + var eventLoop:CustomEventLoop = Thread.current().events; + + var events = []; + while(Internal.mainThreadLoopActive) { + try { + // execute any queued native callbacks + processNativeCalls(); + + // adapted from EventLoop.loop() + var eventTickInfo = eventLoop.customProgress(Sys.time(), events); + switch (eventTickInfo.nextEventAt) { + case -2: // continue to next loop, assume events could have been scheduled + case -1: + if (Internal.mainThreadEndIfNoPending && !eventTickInfo.anyTime) { + // no events scheduled in the future and not waiting on any promises + break; + } + Internal.mainThreadWaitLock.wait(); + case time: + var timeout = time - Sys.time(); + Internal.mainThreadWaitLock.wait(Math.max(0, timeout)); + } + } catch (e: Any) { + onUnhandledException(Std.string(e)); + } + } + + // run a major collection when the thread ends + cpp.vm.Gc.run(true); + } + #else + @:noCompletion + static public function mainThreadInit(isMainThreadCb: cpp.Callable<Void -> Bool>) @:privateAccess { + Internal.isMainThreadCb = isMainThreadCb; + Internal.mainThreadWaitLock = EntryPoint.sleepLock; + } + + @:noCompletion + static public function mainThreadRun(processNativeCalls: cpp.Callable<Void -> Void>, onUnhandledException: cpp.Callable<cpp.ConstCharStar -> Void>) @:privateAccess { + try { + runUserMain(); + } catch (e: Any) { + onUnhandledException(Std.string(e)); + } + + while (Internal.mainThreadLoopActive) { + try { + // execute any queued native callbacks + processNativeCalls(); + + // adapted from EntryPoint.run() + var nextTick = EntryPoint.processEvents(); + if (nextTick < 0) { + if (Internal.mainThreadEndIfNoPending) { + // no events scheduled in the future and not waiting on any promises + break; + } + Internal.mainThreadWaitLock.wait(); + } else if (nextTick > 0) { + Internal.mainThreadWaitLock.wait(nextTick); // wait until nextTick or wakeup() call + } + } catch (e: Any) { + onUnhandledException(Std.string(e)); + } + } + + // run a major collection when the thread ends + cpp.vm.Gc.run(true); + } + #end + + static public inline function retainHaxeArray<T>(haxeArray: Array<T>): HaxeArray<HaxeObject<T>> { + // https://github.com/HaxeFoundation/hxcpp/issues/1092 + //var ptr = cpp.Pointer.ofArray(haxeArray).raw; + var ptr: cpp.RawPointer<cpp.RawPointer<cpp.Void>> = untyped __cpp__('(void**){0}->getBase()', haxeArray); + var ptrInt64: Int64 = untyped __cpp__('reinterpret_cast<int64_t>({0})', ptr); + Internal.gcRetainMap.set(ptrInt64, haxeArray); + return cast ptr; + } + + static public inline function retainHaxeObject(haxeObject: Dynamic): HaxeObject<{}> { + // need to get pointer to object + var ptr: cpp.RawPointer<cpp.Void> = untyped __cpp__('{0}.mPtr', haxeObject); + // we can convert the ptr to int64 + // https://stackoverflow.com/a/21250110 + var ptrInt64: Int64 = untyped __cpp__('reinterpret_cast<int64_t>({0})', ptr); + Internal.gcRetainMap.set(ptrInt64, haxeObject); + return ptr; + } + + static public inline function retainHaxeString(haxeString: String): cpp.ConstCharStar { + var cStrPtr: cpp.ConstCharStar = cpp.ConstCharStar.fromString(haxeString); + var ptrInt64: Int64 = untyped __cpp__('reinterpret_cast<int64_t>({0})', cStrPtr); + Internal.gcRetainMap.set(ptrInt64, haxeString); + return cStrPtr; + } + + static public inline function releaseHaxePtr(haxePtr: Star<cpp.Void>) { + var ptrInt64: Int64 = untyped __cpp__('reinterpret_cast<int64_t>({0})', haxePtr); + Internal.gcRetainMap.remove(ptrInt64); + } + + @:noCompletion + static public inline function isMainThread(): Bool { + return Internal.isMainThreadCb(); + } + + /** not thread-safe, must be called in the haxe main thread **/ + @:noCompletion + static public function endMainThread(waitOnScheduledEvents: Bool) { + Internal.mainThreadEndIfNoPending = true; + Internal.mainThreadLoopActive = Internal.mainThreadLoopActive && waitOnScheduledEvents; + inline wakeMainThread(); + } + + /** called from _unattached_ external thread, must not allocate in hxcpp **/ + @:noDebug + @:noCompletion + static public function wakeMainThread() { + inline Internal.mainThreadWaitLock.release(); + } + + @:noCompletion + static macro function runUserMain() { /* implementation provided above in macro version of HaxeCBridge */ } + +} + +private class Internal { + public static var isMainThreadCb: cpp.Callable<Void -> Bool>; + public static var mainThreadWaitLock: Lock; + public static var mainThreadLoopActive: Bool = true; + public static var mainThreadEndIfNoPending: Bool = false; + public static final gcRetainMap = new Int64Map<Dynamic>(); +} + +/** + Implements an Int64 map via two Int32 maps, using the low and high parts as keys + we need @Aidan63's PR to land before we can use Map<Int64, Dynamic> + https://github.com/HaxeFoundation/hxcpp/pull/932 +**/ +abstract Int64Map<T>(Map<Int, Map<Int, T>>) { + + public function new() { + this = new Map<Int, Map<Int, T>>(); + } + + public inline function set(key: Int64, value: T) { + var low: Int = low32(key); + var high: Int = high32(key); + + // low will vary faster and alias less, so use low as primary key + var highMap = this.get(low); + if (highMap == null) { + highMap = new Map<Int, T>(); + this.set(low, highMap); + } + + highMap.set(high, value); + } + + public inline function get(key: Int64): Null<T> { + var low: Int = low32(key); + var high: Int = high32(key); + var highMap = this.get(low); + return (highMap != null) ? highMap.get(high): null; + } + + public inline function remove(key: Int64): Bool { + var low: Int = low32(key); + var high: Int = high32(key); + var highMap = this.get(low); + + return if (highMap != null) { + var removed = highMap.remove(high); + var isHighMapEmpty = true; + for (k in highMap.keys()) { + isHighMapEmpty = false; + break; + } + // if the high map has no more keys we can dispose of it (so that we don't have empty maps left for unused low keys) + if (isHighMapEmpty) { + this.remove(low); + } + return removed; + } else { + false; + } + } + + inline function high32(key: Int64): Int { + return untyped __cpp__('{0} >> 32', key); + } + + inline function low32(key: Int64): Int { + return untyped __cpp__('{0} & 0xffffffff', key); + } + +} + +#if (haxe_ver >= 4.2) +@:forward +@:access(sys.thread.EventLoop) +abstract CustomEventLoop(sys.thread.EventLoop) from sys.thread.EventLoop { + + // same as __progress but it doesn't reset the wait lock + // this is because resetting the wait lock here can mean wake-up lock releases are missed + // and we cannot resolve by only waking up with in the mutex because this interacts with the hxcpp GC (and we want to wake-up from a non-hxcpp-attached thread) + public inline function customProgress(now:Float, recycle:Array<()->Void>):{nextEventAt:Float, anyTime:Bool} { + var eventsToRun = recycle; + var eventsToRunIdx = 0; + // When the next event is expected to run + var nextEventAt:Float = -1; + + this.mutex.acquire(); + // @edit: don't reset the wait lock (see above) + // while(waitLock.wait(0.0)) {} + // Collect regular events to run + var current = this.regularEvents; + while(current != null) { + if(current.nextRunTime <= now) { + eventsToRun[eventsToRunIdx++] = current.run; + current.nextRunTime += current.interval; + nextEventAt = -2; + } else if(nextEventAt == -1 || current.nextRunTime < nextEventAt) { + nextEventAt = current.nextRunTime; + } + current = current.next; + } + this.mutex.release(); + + // Run regular events + for(i in 0...eventsToRunIdx) { + eventsToRun[i](); + eventsToRun[i] = null; + } + eventsToRunIdx = 0; + + // Collect pending one-time events + this.mutex.acquire(); + for(i => event in this.oneTimeEvents) { + switch event { + case null: + break; + case _: + eventsToRun[eventsToRunIdx++] = event; + this.oneTimeEvents[i] = null; + } + } + this.oneTimeEventsIdx = 0; + var hasPromisedEvents = this.promisedEventsCount > 0; + this.mutex.release(); + + //run events + for(i in 0...eventsToRunIdx) { + eventsToRun[i](); + eventsToRun[i] = null; + } + + // Some events were executed. They could add new events to run. + if(eventsToRunIdx > 0) { + nextEventAt = -2; + } + return {nextEventAt:nextEventAt, anyTime:hasPromisedEvents} + } + +} +#end + +#end diff --git a/cpp.hxml b/cpp.hxml new file mode 100644 index 0000000..18d08c1 --- /dev/null +++ b/cpp.hxml @@ -0,0 +1,14 @@ +--library datetime +--library haxe-strings +--library hsluv +--library tink_http +--library sha + +HaxeCBridge +xmpp.Client +xmpp.Push +xmpp.persistence.Sqlite + +--cpp cpp +-D HaxeCBridge.name=snikket +-D dll_link diff --git a/xmpp/Caps.hx b/xmpp/Caps.hx index f1b0b1c..23e5b9f 100644 --- a/xmpp/Caps.hx +++ b/xmpp/Caps.hx @@ -72,7 +72,7 @@ class Caps { return stanza; } - public function ver(): String { + public function verRaw(): Bytes { features.sort((x, y) -> x == y ? 0 : (x < y ? -1 : 1)); identities.sort((x, y) -> x.ver() == y.ver() ? 0 : (x.ver() < y.ver() ? -1 : 1)); var s = ""; @@ -82,7 +82,11 @@ class Caps { for (feature in features) { s += feature + "<"; } - return Base64.encode(Sha1.make(Bytes.ofString(s)), true); + return Sha1.make(Bytes.ofString(s)); + } + + public function ver(): String { + return Base64.encode(verRaw(), true); } } diff --git a/xmpp/Chat.hx b/xmpp/Chat.hx index a4d7bfa..ff0f82b 100644 --- a/xmpp/Chat.hx +++ b/xmpp/Chat.hx @@ -1,5 +1,8 @@ package xmpp; +#if cpp +import HaxeCBridge; +#end import haxe.io.BytesData; import xmpp.Chat; import xmpp.ChatMessage; @@ -19,22 +22,26 @@ enum UiState { Closed; // Archived } +@:build(HaxeCBridge.expose()) abstract class Chat { private var client:Client; private var stream:GenericStream; private var persistence:Persistence; - private var avatarSha1:Null<BytesData> = null; + @HaxeCBridge.noemit + public var avatarSha1:Null<BytesData> = null; private var presence:Map<String, Presence> = []; private var trusted:Bool = false; public var chatId(default, null):String; public var jingleSessions: Map<String, xmpp.jingle.Session> = []; private var displayName:String; + @HaxeCBridge.noemit public var uiState = Open; - private var extensions: Stanza; + public var extensions: Stanza; private var _unreadCount = 0; private var lastMessage: Null<ChatMessage>; - public function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState = Open, extensions: Null<Stanza> = null) { + @HaxeCBridge.noemit + public function new(client:Client, stream:GenericStream, persistence:Persistence, chatId:String, uiState:Dynamic = Open, extensions: Null<Stanza> = null) { this.client = client; this.stream = stream; this.persistence = persistence; @@ -52,8 +59,10 @@ abstract class Chat { abstract public function getMessages(beforeId:Null<String>, beforeTime:Null<String>, handler:(Array<ChatMessage>)->Void):Void; + @HaxeCBridge.noemit abstract public function getParticipants():Array<String>; + @HaxeCBridge.noemit abstract public function getParticipantDetails(participantId:String, callback:({photoUri:String, displayName:String})->Void):Void; abstract public function bookmark():Void; @@ -152,6 +161,7 @@ abstract class Chat { return presence[resource]?.caps ?? new Caps("", [], []); } + @HaxeCBridge.noemit public function setAvatarSha1(sha1: BytesData) { this.avatarSha1 = sha1; } @@ -190,6 +200,7 @@ abstract class Chat { session.propose(audio, video); } + @HaxeCBridge.noemit public function addMedia(streams: Array<MediaStream>) { if (callStatus() != "ongoing") throw "cannot add media when no call ongoing"; jingleSessions.iterator().next().addMedia(streams); @@ -225,6 +236,7 @@ abstract class Chat { return null; } + @HaxeCBridge.noemit public function videoTracks() { return jingleSessions.flatMap((session) -> session.videoTracks()); } @@ -250,16 +262,20 @@ abstract class Chat { } @:expose +@:build(HaxeCBridge.expose()) class DirectChat extends Chat { - public function getParticipants() { + @HaxeCBridge.noemit + public function getParticipants(): Array<String> { return chatId.split("\n"); } + @HaxeCBridge.noemit public function getParticipantDetails(participantId:String, callback:({photoUri:String, displayName:String})->Void) { final chat = client.getDirectChat(participantId); chat.getPhoto((photoUri) -> callback({ photoUri: photoUri, displayName: chat.getDisplayName() })); } + @HaxeCBridge.noemit // on superclass as abstract public function getMessages(beforeId:Null<String>, beforeTime:Null<String>, handler:(Array<ChatMessage>)->Void):Void { persistence.getMessages(client.accountId(), chatId, beforeId, beforeTime, (messages) -> { if (messages.length > 0) { @@ -322,6 +338,7 @@ class DirectChat extends Chat { }); } + @HaxeCBridge.noemit public function sendMessage(message:ChatMessage):Void { client.chatActivity(this); message = prepareOutgoingMessage(message); @@ -594,10 +611,15 @@ class Channel extends Chat { } public function getMessages(beforeId:Null<String>, beforeTime:Null<String>, handler:(Array<ChatMessage>)->Void):Void { + trace("1"); + return; persistence.getMessages(client.accountId(), chatId, beforeId, beforeTime, (messages) -> { + trace("2"); if (messages.length > 0) { + trace("3"); handler(messages); } else { + trace("4"); var filter:MAMQueryParams = {}; if (beforeId != null) filter.page = { before: beforeId }; var sync = new MessageSync(this.client, this.stream, filter, chatId); diff --git a/xmpp/ChatMessage.hx b/xmpp/ChatMessage.hx index 9b6fe07..529046e 100644 --- a/xmpp/ChatMessage.hx +++ b/xmpp/ChatMessage.hx @@ -31,6 +31,7 @@ class ChatAttachment { @:expose @:nullSafety(Strict) +@:build(HaxeCBridge.expose()) class ChatMessage { public var localId (default, set) : Null<String> = null; public var serverId (default, set) : Null<String> = null; @@ -42,12 +43,15 @@ class ChatMessage { public var to: Null<JID> = null; public var from: Null<JID> = null; public var sender: Null<JID> = null; + @HaxeCBridge.noemit public var recipients: Array<JID> = []; + @HaxeCBridge.noemit public var replyTo: Array<JID> = []; public var replyToMessage: Null<ChatMessage> = null; public var threadId: Null<String> = null; + @HaxeCBridge.noemit public var attachments: Array<ChatAttachment> = []; public var reactions: Map<String, Array<String>> = []; @@ -55,13 +59,21 @@ class ChatMessage { public var lang: Null<String> = null; public var groupchat: Bool = false; // Only really useful for distinguishing whispers + @HaxeCBridge.noemit public var direction: MessageDirection = MessageReceived; + @HaxeCBridge.noemit public var status: MessageStatus = MessagePending; + @HaxeCBridge.noemit public var versions: Array<ChatMessage> = []; + @HaxeCBridge.noemit public var payloads: Array<Stanza> = []; public function new() { } + public function setText(t: String) { + text = t; + } + public static function fromStanza(stanza:Stanza, localJid:JID):Null<ChatMessage> { switch Message.fromStanza(stanza, localJid) { case ChatMessageStanza(message): diff --git a/xmpp/Client.hx b/xmpp/Client.hx index f0754fc..d825b12 100644 --- a/xmpp/Client.hx +++ b/xmpp/Client.hx @@ -1,14 +1,18 @@ package xmpp; +#if cpp +import HaxeCBridge; +#end import sha.SHA256; import haxe.crypto.Base64; import haxe.io.Bytes; import haxe.io.BytesData; -import js.html.rtc.IceServer; // only typedefs, should be portable +import xmpp.jingle.IceServer; import xmpp.Caps; import xmpp.Chat; import xmpp.ChatMessage; +import xmpp.Message; import xmpp.EventEmitter; import xmpp.EventHandler; import xmpp.PubsubEvent; @@ -27,6 +31,7 @@ import xmpp.queries.VcardTempGet; using Lambda; @:expose +@:build(HaxeCBridge.expose()) class Client extends xmpp.EventEmitter { private var stream:GenericStream; private var chatMessageHandlers: Array<(ChatMessage)->Void> = []; @@ -416,7 +421,7 @@ class Client extends xmpp.EventEmitter { this.trigger("chats/update", chats); stream.on("auth/password-needed", (data) -> { - fastMechanism = data.mechanisms.find((mech) -> mech.canFast)?.name; + fastMechanism = data.mechanisms?.find((mech) -> mech.canFast)?.name; if (token == null || fastMechanism == null) { this.trigger("auth/password-needed", { accountId: accountId() }); } else { @@ -434,6 +439,13 @@ class Client extends xmpp.EventEmitter { chatMessageHandlers.push(handler); } + public function addPasswordNeededListener(handler:String->Void) { + this.on("auth/password-needed", (data) -> { + handler(data.accountId); + return EventHandled; + }); + } + private function onConnected(data) { // Fired on connect or reconnect if (data != null && data.jid != null) { jid = JID.parse(data.jid); @@ -482,6 +494,7 @@ class Client extends xmpp.EventEmitter { this.stream.trigger("auth/password", { password: password, requestToken: fastMechanism }); } + #if js public function prepareAttachment(source: js.html.File, callback: (Null<ChatAttachment>)->Void) { // TODO: abstract with filename, mime, and ability to convert to tink.io.Source persistence.findServicesWithFeature(accountId(), "urn:xmpp:http:upload:0", (services) -> { final sha256 = new sha.SHA256(); @@ -520,6 +533,7 @@ class Client extends xmpp.EventEmitter { }); sendQuery(httpUploadSlot); } + #end /* Return array of chats, sorted by last activity */ public function getChats():Array<Chat> { @@ -573,7 +587,8 @@ class Client extends xmpp.EventEmitter { return chat; } - public function findAvailableChats(q:String, callback:(q:String, results:Array<{ chatId: String, fn: String, note: String, caps: Caps }>) -> Void) { + @HaxeCBridge.noemit + public function findAvailableChats(q:String, callback:(String, Array<{ chatId: String, fn: String, note: String, caps: Caps }>) -> Void) { var results = []; final query = StringTools.trim(q); final jid = JID.parse(query); @@ -701,6 +716,7 @@ class Client extends xmpp.EventEmitter { } #end + @HaxeCBridge.noemit public function getIceServers(callback: (Array<IceServer>)->Void) { final extDiscoGet = new ExtDiscoGet(jid.domain); extDiscoGet.onFinished(() -> { @@ -723,6 +739,7 @@ class Client extends xmpp.EventEmitter { sendQuery(extDiscoGet); } + @HaxeCBridge.noemit public function discoverServices(target: JID, ?node: String, callback: ({ jid: JID, name: Null<String>, node: Null<String> }, Caps)->Void) { final itemsGet = new DiscoItemsGet(target.asString(), node); itemsGet.onFinished(()-> { diff --git a/xmpp/Message.hx b/xmpp/Message.hx index b0ce876..9f45357 100644 --- a/xmpp/Message.hx +++ b/xmpp/Message.hx @@ -81,8 +81,9 @@ class Message { if (msg.to != null) { recipients[msg.to.asBare().asString()] = true; } - if (msg.direction == MessageReceived && msg.from != null) { - replyTo[stanza.attr.get("type") == "groupchat" ? msg.from.asBare().asString() : msg.from.asString()] = true; + final from = msg.from; + if (msg.direction == MessageReceived && from != null) { + replyTo[stanza.attr.get("type") == "groupchat" ? from.asBare().asString() : from.asString()] = true; } else if(msg.to != null) { replyTo[msg.to.asString()] = true; } diff --git a/xmpp/Persistence.hx b/xmpp/Persistence.hx index 116c8ba..6a93a5a 100644 --- a/xmpp/Persistence.hx +++ b/xmpp/Persistence.hx @@ -17,11 +17,11 @@ abstract class Persistence { abstract public function getMediaUri(hashAlgorithm:String, hash:BytesData, callback: (uri:Null<String>)->Void):Void; abstract public function storeMedia(mime:String, bytes:BytesData, callback: ()->Void):Void; abstract public function storeCaps(caps:Caps):Void; - abstract public function getCaps(ver:String, callback: (Caps)->Void):Void; + abstract public function getCaps(ver:String, callback: (Null<Caps>)->Void):Void; abstract public function storeLogin(login:String, clientId:String, displayName:String, token:Null<String>):Void; - abstract public function getLogin(login:String, callback:(clientId:String, token:Null<String>, fastCount: Int, displayName:String)->Void):Void; + abstract public function getLogin(login:String, callback:(clientId:Null<String>, token:Null<String>, fastCount: Int, displayName:Null<String>)->Void):Void; abstract public function storeStreamManagement(accountId:String, smId:String, outboundCount:Int, inboundCount:Int, outboundQueue:Array<String>):Void; - abstract public function getStreamManagement(accountId:String, callback: (smId:String, outboundCount:Int, inboundCount:Int, outboundQueue:Array<String>)->Void):Void; + abstract public function getStreamManagement(accountId:String, callback: (smId:Null<String>, outboundCount:Int, inboundCount:Int, outboundQueue:Array<String>)->Void):Void; abstract public function storeService(accountId:String, serviceId:String, name:Null<String>, node:Null<String>, caps:Caps):Void; abstract public function findServicesWithFeature(accountId:String, feature:String, callback:(Array<{serviceId:String, name:Null<String>, node:Null<String>, caps: Caps}>)->Void):Void; } diff --git a/xmpp/PubsubEvent.hx b/xmpp/PubsubEvent.hx new file mode 100644 index 0000000..9f74f16 --- /dev/null +++ b/xmpp/PubsubEvent.hx @@ -0,0 +1,38 @@ +package xmpp; + +class PubsubEvent { + private var from:Null<String>; + private var to:Null<String>; + private var node:String; + private var items:Array<Stanza>; + + public function new(from:Null<String>, to:Null<String>, node:String, items:Array<Stanza>) { + this.from = from; + this.to = to; + this.node = node; + this.items = items; + } + + public static function fromStanza(stanza:Stanza):Null<PubsubEvent> { + var event = stanza.getChild("event", "http://jabber.org/protocol/pubsub#event"); + if (event == null) return null; + + var items = event.getChild("items"); // xmlns is same as event tag + if (items == null) return null; + + // item tag is same xmlns as event and items tag + return new PubsubEvent(stanza.attr.get("from"), stanza.attr.get("to"), items.attr.get("node"), items.allTags("item")); + } + + public function getFrom():Null<String> { + return this.from; + } + + public function getNode():String { + return this.node; + } + + public function getItems():Array<Stanza> { + return this.items; + } +} diff --git a/xmpp/Push.hx b/xmpp/Push.hx index 7511e36..1f614fe 100644 --- a/xmpp/Push.hx +++ b/xmpp/Push.hx @@ -4,13 +4,13 @@ import xmpp.ChatMessage; import xmpp.JID; import xmpp.Notification; import xmpp.Persistence; -import xmpp.Stream; +import xmpp.Stanza; // this code should expect to be called from a different context to the app @:expose function receive(data: String, persistence: Persistence) { - var stanza = Stream.parse(data); + var stanza = Stanza.parse(data); if (stanza == null) return null; if (stanza.name == "envelope" && stanza.attr.get("xmlns") == "urn:xmpp:sce:1") { stanza = stanza.getChild("content").getFirstChild(); diff --git a/xmpp/Stream.cpp.hx b/xmpp/Stream.cpp.hx new file mode 100644 index 0000000..f864419 --- /dev/null +++ b/xmpp/Stream.cpp.hx @@ -0,0 +1,5 @@ +package xmpp; + +import xmpp.streams.XmppStropheStream; + +typedef Stream = xmpp.streams.XmppStropheStream; diff --git a/xmpp/jingle/IceServer.hx b/xmpp/jingle/IceServer.hx new file mode 100644 index 0000000..b372dc6 --- /dev/null +++ b/xmpp/jingle/IceServer.hx @@ -0,0 +1,10 @@ +package xmpp.jingle; +// from js.html.rtc but cross platform + +typedef IceServer = { + var ?credential : String; + // var ?credentialType : IceCredentialType; + var ?url : String; + var ?urls : haxe.extern.EitherType<String,Array<String>>; + var ?username : String; +} diff --git a/xmpp/jingle/PeerConnection.cpp.hx b/xmpp/jingle/PeerConnection.cpp.hx new file mode 100644 index 0000000..29fcbe6 --- /dev/null +++ b/xmpp/jingle/PeerConnection.cpp.hx @@ -0,0 +1,107 @@ +package xmpp.jingle; + +typedef TODO = Dynamic; +typedef MediaStreamTrack = TODO; +typedef DTMFSender = TODO; +typedef Transceiver = { + receiver: Null<{ track: MediaStreamTrack }>, + sender: Null<{ track: MediaStreamTrack, dtmf: DTMFSender }> +} + +class MediaStream { + public function getTracks() { + return []; + } +} + +typedef SessionDescriptionInit = { + var ?sdp : String; + var type : SdpType; +} + +typedef Configuration = { + //var ?bundlePolicy : BundlePolicy; + //var ?certificates : Array<Certificate>; + var ?iceServers : Array<IceServer>; + //var ?iceTransportPolicy : IceTransportPolicy; + var ?peerIdentity : String; +} + +class PeerConnection { + public var localDescription: Dynamic; + + public function new(?configuration : Configuration, ?constraints : Dynamic){ + + } + + public function setLocalDescription(description : SessionDescriptionInit): Promise<Any> { + return new Promise(null); + } + + public function setRemoteDescription(description : SessionDescriptionInit): Promise<Any> { + return new Promise(null); + } + + public function addIceCandidate(candidate : TODO): Promise<Any> { + return new Promise(null); + } + + public function addTrack(track : MediaStreamTrack, stream : MediaStream) { + return null; + } + + public function getTransceivers(): Array<Transceiver> { + return []; + } + + public function close() { } + + public function addEventListener(event: String, callback: Dynamic->Void) { + + } +} + +enum abstract SdpType(String) { + var OFFER = "offer"; + var PRANSWER = "pranswer"; + var ANSWER = "answer"; + var ROLLBACK = "rollback"; +} + +class Promise<T> { + public static function resolve<T>(value: T):Dynamic { // TODO: should be Promise<T> + return new Promise(value); + } + + public static function all<T>(iterable:Array<Promise<T>>): Promise<Array<T>> { + return new Promise([]); + } + + public function new(?value: T) { + + } + + public function then<TOut>(onFulfilled:Null<PromiseHandler<T, TOut>>, ?onRejected:PromiseHandler<Dynamic, TOut>):Promise<TOut> { + return onFulfilled.call(null); + } + + public function catchError(onRejected:PromiseHandler<Dynamic, T>) { + return new Promise(1); + } +} + +abstract PromiseHandler<T, TOut>(T->Promise<TOut>) from T->Promise<TOut> { + @:from + public static function fromVoid<T>(f: T->Void): PromiseHandler<T, Any> { + return (x) -> { f(x); return new Promise(null); }; + } + + @:from + public static function fromNoPromise<T, TOut>(f: T->TOut): PromiseHandler<T, TOut> { + return (x) -> new Promise(f(x)); + } + + public function call(x: T): Promise<TOut> { + return this(x); + } +} diff --git a/xmpp/jingle/Session.hx b/xmpp/jingle/Session.hx index b0530ac..2cf82d7 100644 --- a/xmpp/jingle/Session.hx +++ b/xmpp/jingle/Session.hx @@ -14,7 +14,7 @@ interface Session { public function terminate(): Void; public function contentAdd(stanza: Stanza): Void; public function contentAccept(stanza: Stanza): Void; - public function transportInfo(stanza: Stanza): Promise<Void>; + public function transportInfo(stanza: Stanza): Promise<Any>; public function addMedia(streams: Array<MediaStream>): Void; public function callStatus():String; public function videoTracks():Array<MediaStreamTrack>; @@ -503,7 +503,7 @@ class InitiatedSession implements Session { }) .then((_) -> { setupLocalDescription("session-accept"); - }).then((_) -> { + }).then((x) -> { peerDtlsSetup = localDescription.getDtlsSetup() == "active" ? "passive" : "active"; return; }); diff --git a/xmpp/persistence/Sqlite.hx b/xmpp/persistence/Sqlite.hx new file mode 100644 index 0000000..c54229f --- /dev/null +++ b/xmpp/persistence/Sqlite.hx @@ -0,0 +1,319 @@ +package xmpp.persistence; + +#if cpp +import HaxeCBridge; +#end +import datetime.DateTime; +import haxe.Json; +import haxe.crypto.Base64; +import haxe.crypto.Sha1; +import haxe.crypto.Sha256; +import haxe.io.Bytes; +import haxe.io.BytesData; +import sys.FileSystem; +import sys.db.Connection; +import sys.io.File; +import xmpp.Caps; +import xmpp.Chat; +import xmpp.Message; + +// TODO: consider doing background threads for operations + +@:expose +@:build(HaxeCBridge.expose()) +class Sqlite extends Persistence { + final db: Connection; + final blobpath: String; + + public function new(dbfile: String, blobpath: String) { + this.blobpath = blobpath; + db = sys.db.Sqlite.open(dbfile); + final version = db.request("PRAGMA user_version;").getIntResult(0); + if (version < 1) { + db.request("CREATE TABLE messages ( + account_id TEXT NOT NULL, + mam_id TEXT, + mam_by TEXT, + stanza_id TEXT NOT NULL, + sync_point BOOLEAN NOT NULL, + chat_id TEXT NOT NULL, + created_at INTEGER NOT NULL, + stanza TEXT NOT NULL, + PRIMARY KEY (account_id, mam_id, mam_by) + );"); + db.request("CREATE TABLE chats ( + account_id TEXT NOT NULL, + chat_id TEXT NOT NULL, + trusted BOOLEAN NOT NULL, + avatar_sha1 BLOB, + fn TEXT, + ui_state TEXT, + extensions TEXT, + class TEXT NOT NULL, + PRIMARY KEY (account_id, chat_id) + );"); + db.request("CREATE TABLE media ( + sha256 BLOB NOT NULL PRIMARY KEY, + sha1 BLOB NOT NULL UNIQUE, + mime TEXT NOT NULL + );"); + db.request("CREATE TABLE caps ( + sha1 BLOB NOT NULL UNIQUE, + caps JSONB NOT NULL + );"); + db.request("CREATE TABLE services ( + account_id TEXT NOT NULL, + service_id TEXT NOT NULL, + name TEXT, + node TEXT, + caps BLOB NOT NULL, + PRIMARY KEY (account_id, service_id) + );"); + db.request("PRAGMA user_version = 1;"); + } + } + + public function lastId(accountId: String, chatId: Null<String>, callback:(Null<String>)->Void):Void { + final q = new StringBuf(); + q.add("SELECT mam_id FROM messages WHERE mam_id IS NOT NULL AND sync_point AND account_id="); + db.addValue(q, accountId); + if (chatId != null) { + q.add(" AND chat_id="); + db.addValue(q, chatId); + } + q.add(";"); + try { + callback(db.request(q.toString()).getResult(0)); + } catch (e) { + callback(null); + } + } + + public function storeChat(accountId: String, chat: Chat) { + // TODO: presence + // TODO: disco + trace("storeChat"); + final q = new StringBuf(); + q.add("INSERT OR REPLACE INTO chats VALUES ("); + db.addValue(q, accountId); + q.add(","); + db.addValue(q, chat.chatId); + q.add(","); + db.addValue(q, chat.isTrusted()); + if (chat.avatarSha1 == null) { + q.add(",NULL"); + } else { + q.add(",X"); + db.addValue(q, Bytes.ofData(chat.avatarSha1).toHex()); + } + q.add(","); + db.addValue(q, chat.getDisplayName()); + q.add(","); + db.addValue(q, chat.uiState); + q.add(","); + db.addValue(q, chat.extensions); + q.add(","); + db.addValue(q, Type.getClassName(Type.getClass(chat)).split(".").pop()); + q.add(");"); + db.request(q.toString()); + } + + @HaxeCBridge.noemit + public function getChats(accountId: String, callback: (Array<SerializedChat>)->Void) { + // TODO: presence + // TODO: disco + final q = new StringBuf(); + q.add("SELECT chat_id, trusted, avatar_sha1, fn, ui_state, extensions, class FROM chats WHERE account_id="); + db.addValue(q, accountId); + final result = db.request(q.toString()); + final chats = []; + for (row in result) { + chats.push(new SerializedChat(row.chat_id, row.trusted, row.avatar_sha1, [], row.fn, row.ui_state, row.extensions, null, Reflect.field(row, "class"))); + } + callback(chats); + } + + public function storeMessage(accountId: String, message: ChatMessage, callback: (ChatMessage)->Void) { + final q = new StringBuf(); + q.add("INSERT OR REPLACE INTO messages VALUES ("); + db.addValue(q, accountId); + q.add(","); + db.addValue(q, message.serverId); + q.add(","); + db.addValue(q, message.serverIdBy); + q.add(","); + db.addValue(q, message.localId); + q.add(","); + db.addValue(q, message.syncPoint); + q.add(","); + db.addValue(q, message.chatId()); + q.add(","); + db.addValue(q, DateTime.fromString(message.timestamp).getTime()); + q.add(","); + db.addValue(q, message.asStanza().toString()); + q.add(");"); + db.request(q.toString()); + + // TODO: hydrate reply to stubs? + // TODO: corrections + // TODO: fetch reactions? + callback(message); + } + + @HaxeCBridge.noemit + public function getMessages(accountId: String, chatId: String, beforeId: Null<String>, beforeTime: Null<String>, callback: (Array<ChatMessage>)->Void) { + final q = new StringBuf(); + q.add("SELECT stanza FROM messages WHERE account_id="); + db.addValue(q, accountId); + q.add(" AND chat_id="); + db.addValue(q, chatId); + if (beforeTime != null) { + q.add(" AND created_at <"); + db.addValue(q, DateTime.fromString(beforeTime).getTime()); + } + final result = db.request(q.toString()); + final messages = []; + for (row in result) { + messages.push(ChatMessage.fromStanza(Stanza.parse(row.stanza), JID.parse(accountId))); // TODO + } + callback(messages); + } + + @HaxeCBridge.noemit + public function getChatsUnreadDetails(accountId: String, chats: Array<Chat>, callback: (Array<{ chatId: String, message: ChatMessage, unreadCount: Int }>)->Void) { + callback([]); // TODO + } + + public function storeReaction(accountId: String, update: ReactionUpdate, callback: (Null<ChatMessage>)->Void) { + callback(null); // TODO + } + + @HaxeCBridge.noemit + public function updateMessageStatus(accountId: String, localId: String, status:MessageStatus, callback: (ChatMessage)->Void) { + callback(null); // TODO + } + + @HaxeCBridge.noemit + public function getMediaUri(hashAlgorithm:String, hash:BytesData, callback: (Null<String>)->Void) { + if (hashAlgorithm == "sha-256") { + final path = blobpath + "/f" + Bytes.ofData(hash).toHex(); + if (FileSystem.exists(path)) { + callback("file://" + FileSystem.absolutePath(path)); + } else { + callback(null); + } + } else if (hashAlgorithm == "sha-1") { + final q = new StringBuf(); + q.add("SELECT sha256 FROM media WHERE sha1=X"); + db.addValue(q, Bytes.ofData(hash).toHex()); + q.add(" LIMIT 1"); + final result = db.request(q.toString()); + for (row in result) { + getMediaUri("sha-256", row.sha256, callback); + return; + } + callback(null); + } else { + throw "Unknown hash algorithm: " + hashAlgorithm; + } + } + + @HaxeCBridge.noemit + public function storeMedia(mime:String, bd:BytesData, callback: ()->Void) { + final bytes = Bytes.ofData(bd); + final sha256 = Sha256.make(bytes).toHex(); + final sha1 = Sha1.make(bytes).toHex(); + File.saveBytes(blobpath + "/f" + sha256, bytes); + + final q = new StringBuf(); + q.add("INSERT OR IGNORE INTO media VALUES (X"); + db.addValue(q, sha256); + q.add(",X"); + db.addValue(q, sha1); + q.add(","); + db.addValue(q, mime); + q.add(");"); + db.request(q.toString()); + + callback(); + } + + public function storeCaps(caps:Caps) { + final q = new StringBuf(); + q.add("INSERT OR IGNORE INTO caps VALUES (X"); + db.addValue(q, caps.verRaw().toHex()); + q.add(",jsonb("); + db.addValue(q, Json.stringify(caps)); + q.add("));"); + db.request(q.toString()); + } + + public function getCaps(ver:String, callback: (Caps)->Void) { + final q = new StringBuf(); + q.add("SELECT json(caps) AS caps FROM caps WHERE sha1=X"); + db.addValue(q, Base64.decode(ver).toHex()); + q.add(" LIMIT 1"); + final result = db.request(q.toString()); + for (row in result) { + final json = Json.parse(row.caps); + callback(new Caps(json.node, json.identities.map(i -> new Identity(i.category, i.type, i.name)), json.features)); + return; + } + callback(null); + } + + public function storeLogin(login:String, clientId:String, displayName:String, token:Null<String>) { + // TODO + } + + public function getLogin(login:String, callback:(Null<String>, Null<String>, Int, Null<String>)->Void) { + // TODO + callback(null, null, 0, null); + } + + @HaxeCBridge.noemit + public function storeStreamManagement(accountId:String, smId:String, outboundCount:Int, inboundCount:Int, outboundQueue:Array<String>) { + // TODO + } + + @HaxeCBridge.noemit + public function getStreamManagement(accountId:String, callback: (Null<String>, Int, Int, Array<String>)->Void) { + callback(null, -1, -1, []); // TODO + } + + public function storeService(accountId:String, serviceId:String, name:Null<String>, node:Null<String>, caps:Caps) { + storeCaps(caps); + + final q = new StringBuf(); + q.add("INSERT OR REPLACE INTO services VALUES ("); + db.addValue(q, accountId); + q.add(","); + db.addValue(q, serviceId); + q.add(","); + db.addValue(q, name); + q.add(","); + db.addValue(q, node); + q.add(",X"); + db.addValue(q, caps.verRaw().toHex()); + q.add(");"); + db.request(q.toString()); + } + + @HaxeCBridge.noemit + public function findServicesWithFeature(accountId:String, feature:String, callback:(Array<{serviceId:String, name:Null<String>, node:Null<String>, caps: Caps}>)->Void) { + // Almost full scan shouldn't be too expensive, how many services are we aware of? + final q = new StringBuf(); + q.add("SELECT service_id, name, node, json(caps.caps) AS caps FROM services INNER JOIN caps ON services.caps=caps.sha1 WHERE account_id="); + db.addValue(q, accountId); + final result = db.request(q.toString()); + final services = []; + for (row in result) { + final json = Json.parse(row.caps); + if (json.features.contains(feature)) { + row.set("caps", new Caps(json.node, json.identities.map(i -> new Identity(i.category, i.type, i.name)), json.features)); + services.push(row); + } + } + callback(services); + } +} diff --git a/xmpp/persistence/browser.js b/xmpp/persistence/browser.js index f635992..98019b9 100644 --- a/xmpp/persistence/browser.js +++ b/xmpp/persistence/browser.js @@ -483,7 +483,7 @@ exports.xmpp.persistence = { }, (e) => { console.error(e); - callback(null, -1, -1); + callback(null, -1, -1, []); } ); }, @@ -503,7 +503,7 @@ exports.xmpp.persistence = { callback(result[0], result[1], result[2] || 0, result[3]); }).catch((e) => { console.error(e); - callback(null, null, null); + callback(null, null, 0, null); }); }, diff --git a/xmpp/streams/XmppStropheStream.hx b/xmpp/streams/XmppStropheStream.hx new file mode 100644 index 0000000..1126dd7 --- /dev/null +++ b/xmpp/streams/XmppStropheStream.hx @@ -0,0 +1,324 @@ +package xmpp.streams; + +import haxe.DynamicAccess; + +import cpp.Char; +import cpp.ConstPointer; +import cpp.Function; +import cpp.NativeArray; +import cpp.NativeGc; +import cpp.NativeString; +import cpp.RawConstPointer; +import cpp.RawPointer; + +import xmpp.GenericStream; +import xmpp.ID; +import xmpp.Stanza; + +@:include("strophe.h") +@:native("xmpp_mem_t*") +extern class StropheMem { } + +@:include("strophe.h") +@:native("xmpp_log_t*") +extern class StropheLog { } + +@:include("strophe.h") +@:native("xmpp_conn_event_t") +extern class StropheConnEvent { } + +@:include("strophe.h") +@:native("xmpp_stream_error_t*") +extern class StropheStreamError { } + +@:include("strophe.h") +@:native("xmpp_ctx_t*") +extern class StropheCtx { + @:native("xmpp_ctx_new") + static function create(mem:StropheMem, log:StropheLog):StropheCtx; + + @:native("xmpp_ctx_free") + static function free(ctx:StropheCtx):Void; + + @:native("xmpp_initialize") + static function initialize():Void; + + @:native("xmpp_run") + static function run(ctx:StropheCtx):Void; + + @:native("xmpp_run_once") + static function run_once(ctx:StropheCtx, timeout:cpp.UInt64):Void; + + @:native("xmpp_stop") + static function stop(ctx:StropheCtx):Void; +} + +@:include("strophe.h") +@:native("xmpp_conn_t*") +extern class StropheConn { + @:native("xmpp_conn_new") + static function create(ctx:StropheCtx):StropheConn; + + @:native("xmpp_conn_set_jid") + static function set_jid(conn:StropheConn, jid:ConstPointer<Char>):Void; + + @:native("xmpp_conn_set_pass") + static function set_pass(conn:StropheConn, pass:ConstPointer<Char>):Void; + + @:native("xmpp_connect_client") + static function connect_client( + conn:StropheConn, + altdomain:ConstPointer<Char>, + altport:cpp.UInt16, + callback:cpp.Callable<StropheConn->StropheConnEvent->cpp.Int32->StropheStreamError->RawPointer<Void>->Void>, + userdata:RawPointer<Void> + ):cpp.Int32; + + @:native("xmpp_handler_add") + static function handler_add( + conn:StropheConn, + handler:cpp.Callable<StropheConn->StropheStanza->RawPointer<Void>->Int>, + altdomain:ConstPointer<Char>, + altdomain:ConstPointer<Char>, + altdomain:ConstPointer<Char>, + userdata:RawPointer<Void> + ):cpp.Int32; + + @:native("xmpp_send") + static function send(conn:StropheConn, stanza:StropheStanza):Void; + + @:native("xmpp_conn_release") + static function release(conn:StropheConn):Void; +} + + +@:include("strophe.h") +@:native("xmpp_stanza_t*") +extern class StropheStanza { + @:native("xmpp_stanza_new") + static function create(ctx:StropheCtx):StropheStanza; + + @:native("xmpp_stanza_get_name") + static function get_name(stanza:StropheStanza):ConstPointer<Char>; + + @:native("xmpp_stanza_get_attribute_count") + static function get_attribute_count(stanza:StropheStanza):Int; + + @:native("xmpp_stanza_get_attributes") + static function get_attributes(stanza:StropheStanza, attr:RawPointer<RawConstPointer<Char>>, attrlen:Int):Int; + + @:native("xmpp_stanza_get_children") + static function get_children(stanza:StropheStanza):StropheStanza; + + @:native("xmpp_stanza_get_next") + static function get_next(stanza:StropheStanza):StropheStanza; + + @:native("xmpp_stanza_is_text") + static function is_text(stanza:StropheStanza):Bool; + + @:native("xmpp_stanza_get_text_ptr") + static function get_text_ptr(stanza:StropheStanza):RawConstPointer<Char>; + + @:native("xmpp_stanza_set_name") + static function set_name(stanza:StropheStanza, name:ConstPointer<Char>):Void; + + @:native("xmpp_stanza_set_attribute") + static function set_attribute(stanza:StropheStanza, key:ConstPointer<Char>, value:ConstPointer<Char>):Void; + + @:native("xmpp_stanza_add_child_ex") + static function add_child_ex(stanza:StropheStanza, child:StropheStanza, clone:Bool):Void; + + @:native("xmpp_stanza_set_text") + static function set_text(stanza:StropheStanza, text:ConstPointer<Char>):Void; + + @:native("xmpp_stanza_release") + static function release(stanza:StropheStanza):Void; +} + +@:buildXml(" +<target id='haxe'> + <lib name='-lstrophe'/> +</target> +") +@:headerInclude("strophe.h") +@:headerClassCode(" + private: xmpp_ctx_t *ctx; + private: xmpp_conn_t *conn; +") +class XmppStropheStream extends GenericStream { + extern private var ctx:StropheCtx; + extern private var conn:StropheConn; + private var iqHandlers: Map<IqRequestType, Map<String, Stanza->IqResult>> = [IqRequestType.Get => [], IqRequestType.Set => []]; + + override public function new() { + super(); + StropheCtx.initialize(); // TODO: shutdown? + untyped __cpp__("xmpp_log_t *logger = xmpp_get_default_logger(XMPP_LEVEL_DEBUG);"); + ctx = StropheCtx.create(null, untyped __cpp__("logger")); + conn = StropheConn.create(ctx); + StropheConn.handler_add( + conn, + cpp.Callable.fromStaticFunction(strophe_stanza), + null, + null, + null, + untyped __cpp__("(void*)this") + ); + NativeGc.addFinalizable(this, false); + } + + public function newId():String { + return ID.long(); + } + + public static function strophe_stanza(conn:StropheConn, sstanza:StropheStanza, userdata:RawPointer<Void>):Int { + final stream: XmppStropheStream = untyped __cpp__("static_cast<hx::Object*>(userdata)"); + final stanza = convertToStanza(sstanza, null); + + final xmlns = stanza.attr.get("xmlns"); + if(xmlns == "jabber:client") { + final name = stanza.name; + if(name == "iq") { + final type = stanza.attr.get("type"); + if(type == "result" || type == "error") { + stream.onStanza(stanza); + } else { + // These are handled by onIq instead + final child = stanza.getFirstChild(); + if (child != null) { + final handler = stream.iqHandlers[type == "get" ? IqRequestType.Get : IqRequestType.Set]["{" + child.attr.get("xmlns") + "}" + child.name]; + if (handler != null) { + final reply = new Stanza("iq", { type: "result", from: stanza.attr.get("to"), to: stanza.attr.get("from"), id: stanza.attr.get("id") }); + try { + switch(handler(stanza)) { + case IqResultElement(el): reply.addChild(el); + case IqResult: // Empty success reply + case IqNoResult: + reply.attr.set("result", "error"); + reply.tag("error", { type: "cancel" }).tag("service-unavailable", { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" }); + } + } catch (e) { + reply.attr.set("result", "error"); + reply.tag("error", { type: "cancel" }).tag("internal-server-error", { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" }); + } + stream.sendStanza(reply); + } + } + } + } else { + stream.onStanza(stanza); + } + } + + return 1; + } + + public function onIq(type:IqRequestType, tag:String, xmlns:String, handler:(Stanza)->IqResult) { + iqHandlers[type]["{" + xmlns + "}" + tag] = handler; + } + + public static function strophe_connect(conn:StropheConn, event:StropheConnEvent, error:cpp.Int32, stream_error:StropheStreamError, userdata:RawPointer<Void>) { + var stream: XmppStropheStream = untyped __cpp__("static_cast<hx::Object*>(userdata)"); + if (event == untyped __cpp__("XMPP_CONN_CONNECT")) { + stream.trigger("status/online", {}); + } + if (event == untyped __cpp__("XMPP_CONN_DISCONNECT")) { + stream.trigger("status/offline", {}); + } + if (event == untyped __cpp__("XMPP_CONN_FAIL")) { + stream.trigger("status/offline", {}); + } + } + + public function connect(jid:String, sm:Null<{id:String,outbound:Int,inbound:Int,outbound_q:Array<String>}>) { + StropheConn.set_jid(conn, NativeString.c_str(jid)); + this.on("auth/password", function (event) { + var o = this; + var pass = event.password; + StropheConn.set_pass(conn, NativeString.c_str(pass)); + StropheConn.connect_client( + this.conn, + null, + 0, + cpp.Callable.fromStaticFunction(strophe_connect), + untyped __cpp__("o.GetPtr()") + ); + + return EventHandled; + }); + this.trigger("auth/password-needed", {}); + poll(); + } + + private function poll() { + sys.thread.Thread.current().events.run(() -> { + StropheCtx.run_once(ctx, 1); + poll(); + }); + } + + public static function convertToStanza(el:StropheStanza, dummy:RawPointer<Void>):Stanza { + var name = StropheStanza.get_name(el); + var attrlen = StropheStanza.get_attribute_count(el); + var attrsraw: RawPointer<cpp.Void> = NativeGc.allocGcBytesRaw(attrlen * 2 * untyped __cpp__("sizeof(char*)"), false); + var attrsarray: RawPointer<RawConstPointer<Char>> = untyped __cpp__("static_cast<const char**>(attrsraw)"); + var attrsptr = cpp.Pointer.fromRaw(attrsarray); + StropheStanza.get_attributes(el, attrsarray, attrlen * 2); + var attrs: DynamicAccess<String> = {}; + for (i in 0...attrlen) { + var key = ConstPointer.fromRaw(attrsptr.at(i*2)); + var value = ConstPointer.fromRaw(attrsptr.at((i*2)+1)); + attrs[NativeString.fromPointer(key)] = NativeString.fromPointer(value); + } + var stanza = new Stanza(NativeString.fromPointer(name), attrs); + + var child = StropheStanza.get_children(el); + while(child != null) { + if (StropheStanza.is_text(child)) { + var r = StropheStanza.get_text_ptr(child); + var x = NativeString.fromPointer(ConstPointer.fromRaw(StropheStanza.get_text_ptr(child))); + stanza.text(x); + } else { + stanza.addChild(convertToStanza(child, null)); + } + child = StropheStanza.get_next(child); + } + + return stanza; + } + + private function convertFromStanza(el:Stanza):StropheStanza { + var xml = StropheStanza.create(ctx); + StropheStanza.set_name(xml, NativeString.c_str(el.name)); + for (attr in el.attr.keyValueIterator()) { + var key = attr.key; + var value = attr.value; + if (value != null) { + StropheStanza.set_attribute(xml, NativeString.c_str(key), NativeString.c_str(value)); + } + } + if(el.children.length > 0) { + for(child in el.children) { + switch(child) { + case Element(stanza): + StropheStanza.add_child_ex(xml, convertFromStanza(stanza), false); + case CData(text): + var text_node = StropheStanza.create(ctx); + StropheStanza.set_text(text_node, NativeString.c_str(text.serialize())); + StropheStanza.add_child_ex(xml, text_node, false); + }; + } + } + return xml; + } + + public function sendStanza(stanza:Stanza) { + StropheConn.send(conn, convertFromStanza(stanza)); + } + + public function finalize() { + StropheCtx.stop(ctx); + StropheConn.release(conn); + StropheCtx.free(ctx); + } +}