diff --git a/bindings/dart/lib/bindings.dart b/bindings/dart/lib/bindings.dart index b14c8c881..41315ebf9 100644 --- a/bindings/dart/lib/bindings.dart +++ b/bindings/dart/lib/bindings.dart @@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart' show kReleaseMode; export 'bindings.g.dart'; -final bindings = Bindings(_defaultLib()); +Bindings? bindings; typedef PostCObject = Int8 Function(Int64, Pointer); @@ -86,6 +86,10 @@ class Bindings { .lookup>('free_string') .asFunction(); + static Bindings loadDefault() { + return Bindings(_defaultLib()); + } + final session_create_dart session_create; final session_channel_send_dart session_channel_send; final session_close_dart session_close; diff --git a/bindings/dart/lib/internal/direct_client.dart b/bindings/dart/lib/internal/direct_client.dart index 55e763802..598bb6dbe 100644 --- a/bindings/dart/lib/internal/direct_client.dart +++ b/bindings/dart/lib/internal/direct_client.dart @@ -18,8 +18,9 @@ class DirectClient extends Client { int _handle; final Stream _stream; final MessageMatcher _messageMatcher = MessageMatcher(); + final Bindings _bindings; - DirectClient(this._handle, ReceivePort port) : _stream = port.cast(), super() { + DirectClient(this._handle, ReceivePort port, this._bindings) : _stream = port.cast(), super() { unawaited(_receive()); } @@ -50,7 +51,7 @@ class DirectClient extends Client { _messageMatcher.close(); await _invokeNativeAsync( - (port) => bindings.session_close( + (port) => _bindings.session_close( handle, NativeApi.postCObject, port, @@ -68,7 +69,7 @@ class DirectClient extends Client { _messageMatcher.close(); - bindings.session_close_blocking(handle); + _bindings.session_close_blocking(handle); } Future _receive() async { @@ -79,7 +80,7 @@ class DirectClient extends Client { Future copyToRawFd(int fileHandle, int fd) { return _invokeNativeAsync( - (port) => bindings.file_copy_to_raw_fd( + (port) => _bindings.file_copy_to_raw_fd( handle, fileHandle, fd, @@ -98,7 +99,7 @@ class DirectClient extends Client { try { buffer.asTypedList(data.length).setAll(0, data); - bindings.session_channel_send(_handle, buffer, data.length); + _bindings.session_channel_send(_handle, buffer, data.length); } finally { malloc.free(buffer); } diff --git a/bindings/dart/lib/ouisync.dart b/bindings/dart/lib/ouisync.dart index 28d711bb4..493f365d3 100644 --- a/bindings/dart/lib/ouisync.dart +++ b/bindings/dart/lib/ouisync.dart @@ -52,7 +52,12 @@ class Session { } final recvPort = ReceivePort(); - final result = _withPoolSync((pool) => bindings.session_create( + + if (bindings == null) { + bindings = Bindings.loadDefault(); + } + + final result = _withPoolSync((pool) => bindings!.session_create( kind.encode(), pool.toNativeUtf8(configPath), logPath != null ? pool.toNativeUtf8(logPath) : nullptr, @@ -71,7 +76,7 @@ class Session { throw Error(errorCode, errorMessage); } - final client = DirectClient(handle, recvPort); + final client = DirectClient(handle, recvPort, bindings!); return Session._(client); } @@ -923,7 +928,7 @@ class File { /// Print log message void logPrint(LogLevel level, String scope, String message) => - _withPoolSync((pool) => bindings.log_print( + _withPoolSync((pool) => bindings!.log_print( level.encode(), pool.toNativeUtf8(scope), pool.toNativeUtf8(message), @@ -992,5 +997,5 @@ extension Utf8Pointer on Pointer { // Free a pointer that was allocated by the native side. void freeString(Pointer ptr) { - bindings.free_string(ptr.cast()); + bindings!.free_string(ptr.cast()); } diff --git a/bindings/swift/OuisyncLib/Package.swift b/bindings/swift/OuisyncLib/Package.swift index 78e5691ef..f47e53d25 100644 --- a/bindings/swift/OuisyncLib/Package.swift +++ b/bindings/swift/OuisyncLib/Package.swift @@ -22,7 +22,9 @@ let package = Package( name: "OuisyncLib", dependencies: [.product(name:"MessagePack", package: "MessagePack.swift"), "OuisyncLibFFI"] ), - .target(name: "OuisyncLibFFI", dependencies: []), + .target(name: "OuisyncLibFFI", dependencies: ["OuisyncDyLibBuilder"]), + .plugin(name: "OuisyncDyLibBuilder", capability: .buildTool()), + .testTarget( name: "OuisyncLibTests", dependencies: ["OuisyncLib"]), diff --git a/bindings/swift/OuisyncLib/Plugins/OuisyncDyLibBuilder/OuisyncDyLibBuilder.swift b/bindings/swift/OuisyncLib/Plugins/OuisyncDyLibBuilder/OuisyncDyLibBuilder.swift new file mode 100644 index 000000000..f8136ef70 --- /dev/null +++ b/bindings/swift/OuisyncLib/Plugins/OuisyncDyLibBuilder/OuisyncDyLibBuilder.swift @@ -0,0 +1,89 @@ +// +// File.swift +// +// +// Created by Peter Jankuliak on 26/07/2024. +// + +// A package plugin which builds the libouisync_ffi.dylib library and includes it in the package. +// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md +// +// TODO: Right now it creates only Debug version of the library and only for the current architecture. +// Can we detect the build type and architecture here? This github ticket seems to indicate it's not +// currently possible: +// https://github.com/swiftlang/swift-package-manager/issues/7110 + +import Foundation +import PackagePlugin + +@main +struct OuisyncDyLibBuilder: BuildToolPlugin { + func createBuildCommands(context: PackagePlugin.PluginContext, target: PackagePlugin.Target) async throws -> [PackagePlugin.Command] { + let workDir = context.pluginWorkDirectory + let dylibName = "libouisync_ffi.dylib" + let dylibPath = workDir.appending("debug").appending(dylibName) + let cargoPath = shell("which cargo").trimmingCharacters(in: .whitespacesAndNewlines) + let inputFiles = findInputFiles() + + return [ + .buildCommand( + displayName: "Build Ouisync FFI .dylib", + executable: Path(cargoPath), + arguments: ["build", "-p", "ouisync-ffi", "--target-dir", workDir], + environment: [:], + inputFiles: inputFiles.map { Path($0) }, + outputFiles: [ dylibPath ]) + ] + } +} + +// This finds files which when changed, the Swift builder will re-execute the build above build command. +// The implementation is not very good because if a new directory/module is added on which this package +// depends, the package builder won't rebuild the ffi library. +// TODO: use `cargo build --build-plan` once it's in stable. +func findInputFiles() -> [String] { + var files = [String]() + let startAts = [ + URL(string: "../../../bridge")!, + URL(string: "../../../deadlock")!, + URL(string: "../../../ffi")!, + URL(string: "../../../lib")!, + URL(string: "../../../net")!, + URL(string: "../../../rand")!, + URL(string: "../../../scoped_task")!, + URL(string: "../../../state_monitor")!, + URL(string: "../../../tracing_fmt")!, + URL(string: "../../../utils")!, + URL(string: "../../../vfs")!, + ] + for startAt in startAts { + if let enumerator = FileManager.default.enumerator(at: startAt, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) { + for case let fileURL as URL in enumerator { + do { + let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey]) + if fileAttributes.isRegularFile! { + files.append(fileURL.path) + } + } catch { fatalError("Error finding input files: \(error), \(fileURL)") } + } + } + } + return files +} + +func shell(_ command: String) -> String { + let task = Process() + let pipe = Pipe() + + task.standardOutput = pipe + task.standardError = pipe + task.arguments = ["-c", command] + task.launchPath = "/bin/zsh" + task.standardInput = nil + task.launch() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8)! + + return output +} diff --git a/bindings/swift/OuisyncLib/Sources/OuisyncLib/OuisyncFFI.swift b/bindings/swift/OuisyncLib/Sources/OuisyncLib/OuisyncFFI.swift index 960aa220f..0c763a11b 100644 --- a/bindings/swift/OuisyncLib/Sources/OuisyncLib/OuisyncFFI.swift +++ b/bindings/swift/OuisyncLib/Sources/OuisyncLib/OuisyncFFI.swift @@ -31,7 +31,11 @@ public class OuisyncFFI { let sessionKindShared: FFISessionKind = 0; public init() { - handle = dlopen("libouisync_ffi.dylib", RTLD_NOW)! + // The .dylib is created using the OuisyncDyLibBuilder package plugin in this Swift package. + let libraryName = "libouisync_ffi.dylib" + let resourcePath = Bundle.main.resourcePath! + "/OuisyncLib_OuisyncLibFFI.bundle/Contents/Resources" + handle = dlopen("\(resourcePath)/\(libraryName)", RTLD_NOW)! + ffiSessionGrab = unsafeBitCast(dlsym(handle, "session_grab"), to: FFISessionGrab.self) ffiSessionChannelSend = unsafeBitCast(dlsym(handle, "session_channel_send"), to: FFISessionChannelSend.self) ffiSessionClose = unsafeBitCast(dlsym(handle, "session_close"), to: FFISessionClose.self) @@ -82,28 +86,3 @@ public class OuisyncFFI { return Unmanaged.fromOpaque(ptr).takeRetainedValue() } } - -// --------------------------------------------------------------------------------------- - -typealias Rx = AsyncStream<[UInt8]> -typealias Tx = AsyncStream<[UInt8]>.Continuation - -class Wrap { - let inner: T - init(_ inner: T) { self.inner = inner } -} - -class Channel { - let rx: Rx - let tx: Tx - - init(_ rx: Rx, _ tx: Tx) { self.rx = rx; self.tx = tx } -} - -func makeStream() -> (Rx, Tx) { - var continuation: Rx.Continuation! - let stream = Rx() { continuation = $0 } - return (stream, continuation!) -} - -// ---------------------------------------------------------------------------------------