From 96196ab8bc05f31b09dac2403f9d5de3bc05f31b Mon Sep 17 00:00:00 2001
From: Pablo Lopez
Date: Mon, 22 Dec 2025 15:40:09 +0200
Subject: [PATCH] feat: compilation for iOS WIP (#3668)
* feat: compilation for iOS WIP
* fix: nim ios version 18
---
.gitignore | 10 +
Makefile | 45 ++
.../ios/WakuExample.xcodeproj/project.pbxproj | 331 ++++++++
.../contents.xcworkspacedata | 7 +
examples/ios/WakuExample/ContentView.swift | 229 ++++++
examples/ios/WakuExample/Info.plist | 36 +
.../WakuExample/WakuExample-Bridging-Header.h | 15 +
examples/ios/WakuExample/WakuExampleApp.swift | 19 +
examples/ios/WakuExample/WakuNode.swift | 739 ++++++++++++++++++
examples/ios/WakuExample/libwaku.h | 253 ++++++
examples/ios/project.yml | 47 ++
library/ios_bearssl_stubs.c | 32 +
library/ios_natpmp_stubs.c | 14 +
waku.nimble | 179 +++++
14 files changed, 1956 insertions(+)
create mode 100644 examples/ios/WakuExample.xcodeproj/project.pbxproj
create mode 100644 examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
create mode 100644 examples/ios/WakuExample/ContentView.swift
create mode 100644 examples/ios/WakuExample/Info.plist
create mode 100644 examples/ios/WakuExample/WakuExample-Bridging-Header.h
create mode 100644 examples/ios/WakuExample/WakuExampleApp.swift
create mode 100644 examples/ios/WakuExample/WakuNode.swift
create mode 100644 examples/ios/WakuExample/libwaku.h
create mode 100644 examples/ios/project.yml
create mode 100644 library/ios_bearssl_stubs.c
create mode 100644 library/ios_natpmp_stubs.c
diff --git a/.gitignore b/.gitignore
index 7430c3e99..f03c4ebaf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,6 +59,10 @@ nimbus-build-system.paths
/examples/nodejs/build/
/examples/rust/target/
+# Xcode user data
+xcuserdata/
+*.xcuserstate
+
# Coverage
coverage_html_report/
@@ -79,3 +83,9 @@ waku_handler.moc.cpp
# Nix build result
result
+
+# llms
+AGENTS.md
+nimble.develop
+nimble.paths
+nimbledeps
diff --git a/Makefile b/Makefile
index 35c107d2d..87bd7bc74 100644
--- a/Makefile
+++ b/Makefile
@@ -517,6 +517,51 @@ libwaku-android:
# It's likely this architecture is not used so we might just not support it.
# $(MAKE) libwaku-android-arm
+#################
+## iOS Bindings #
+#################
+.PHONY: libwaku-ios-precheck \
+ libwaku-ios-device \
+ libwaku-ios-simulator \
+ libwaku-ios
+
+IOS_DEPLOYMENT_TARGET ?= 18.0
+
+# Get SDK paths dynamically using xcrun
+define get_ios_sdk_path
+$(shell xcrun --sdk $(1) --show-sdk-path 2>/dev/null)
+endef
+
+libwaku-ios-precheck:
+ifeq ($(detected_OS),Darwin)
+ @command -v xcrun >/dev/null 2>&1 || { echo "Error: Xcode command line tools not installed"; exit 1; }
+else
+ $(error iOS builds are only supported on macOS)
+endif
+
+# Build for iOS architecture
+build-libwaku-for-ios-arch:
+ IOS_SDK=$(IOS_SDK) IOS_ARCH=$(IOS_ARCH) IOS_SDK_PATH=$(IOS_SDK_PATH) $(ENV_SCRIPT) nim libWakuIOS $(NIM_PARAMS) waku.nims
+
+# iOS device (arm64)
+libwaku-ios-device: IOS_ARCH=arm64
+libwaku-ios-device: IOS_SDK=iphoneos
+libwaku-ios-device: IOS_SDK_PATH=$(call get_ios_sdk_path,iphoneos)
+libwaku-ios-device: | libwaku-ios-precheck build deps
+ $(MAKE) build-libwaku-for-ios-arch IOS_ARCH=$(IOS_ARCH) IOS_SDK=$(IOS_SDK) IOS_SDK_PATH=$(IOS_SDK_PATH)
+
+# iOS simulator (arm64 - Apple Silicon Macs)
+libwaku-ios-simulator: IOS_ARCH=arm64
+libwaku-ios-simulator: IOS_SDK=iphonesimulator
+libwaku-ios-simulator: IOS_SDK_PATH=$(call get_ios_sdk_path,iphonesimulator)
+libwaku-ios-simulator: | libwaku-ios-precheck build deps
+ $(MAKE) build-libwaku-for-ios-arch IOS_ARCH=$(IOS_ARCH) IOS_SDK=$(IOS_SDK) IOS_SDK_PATH=$(IOS_SDK_PATH)
+
+# Build all iOS targets
+libwaku-ios:
+ $(MAKE) libwaku-ios-device
+ $(MAKE) libwaku-ios-simulator
+
cwaku_example: | build libwaku
echo -e $(BUILD_MSG) "build/$@" && \
cc -o "build/$@" \
diff --git a/examples/ios/WakuExample.xcodeproj/project.pbxproj b/examples/ios/WakuExample.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..b7ce1dce7
--- /dev/null
+++ b/examples/ios/WakuExample.xcodeproj/project.pbxproj
@@ -0,0 +1,331 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 63;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 45714AF6D1D12AF5C36694FB /* WakuExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0671AF6DCB0D788B0C1E9C8B /* WakuExampleApp.swift */; };
+ 6468FA3F5F760D3FCAD6CDBF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D8744E36DADC11F38A1CC99 /* ContentView.swift */; };
+ C4EA202B782038F96336401F /* WakuNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A565C495A63CFF7396FBC /* WakuNode.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 0671AF6DCB0D788B0C1E9C8B /* WakuExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WakuExampleApp.swift; sourceTree = ""; };
+ 31BE20DB2755A11000723420 /* libwaku.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libwaku.h; sourceTree = ""; };
+ 5C5AAC91E0166D28BFA986DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
+ 638A565C495A63CFF7396FBC /* WakuNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WakuNode.swift; sourceTree = ""; };
+ 7D8744E36DADC11F38A1CC99 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ A8655016B3DF9B0877631CE5 /* WakuExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WakuExample-Bridging-Header.h"; sourceTree = ""; };
+ CFBE844B6E18ACB81C65F83B /* WakuExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WakuExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXGroup section */
+ 34547A6259485BD047D6375C /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ CFBE844B6E18ACB81C65F83B /* WakuExample.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 4F76CB85EC44E951B8E75522 /* WakuExample */ = {
+ isa = PBXGroup;
+ children = (
+ 7D8744E36DADC11F38A1CC99 /* ContentView.swift */,
+ 5C5AAC91E0166D28BFA986DB /* Info.plist */,
+ 31BE20DB2755A11000723420 /* libwaku.h */,
+ A8655016B3DF9B0877631CE5 /* WakuExample-Bridging-Header.h */,
+ 0671AF6DCB0D788B0C1E9C8B /* WakuExampleApp.swift */,
+ 638A565C495A63CFF7396FBC /* WakuNode.swift */,
+ );
+ path = WakuExample;
+ sourceTree = "";
+ };
+ D40CD2446F177CAABB0A747A = {
+ isa = PBXGroup;
+ children = (
+ 4F76CB85EC44E951B8E75522 /* WakuExample */,
+ 34547A6259485BD047D6375C /* Products */,
+ );
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ F751EF8294AD21F713D47FDA /* WakuExample */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 757FA0123629BD63CB254113 /* Build configuration list for PBXNativeTarget "WakuExample" */;
+ buildPhases = (
+ D3AFD8C4DA68BF5C4F7D8E10 /* Sources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = WakuExample;
+ packageProductDependencies = (
+ );
+ productName = WakuExample;
+ productReference = CFBE844B6E18ACB81C65F83B /* WakuExample.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 4FF82F0F4AF8E1E34728F150 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1500;
+ };
+ buildConfigurationList = B3A4F48294254543E79767C4 /* Build configuration list for PBXProject "WakuExample" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ Base,
+ en,
+ );
+ mainGroup = D40CD2446F177CAABB0A747A;
+ minimizedProjectReferenceProxies = 1;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ F751EF8294AD21F713D47FDA /* WakuExample */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXSourcesBuildPhase section */
+ D3AFD8C4DA68BF5C4F7D8E10 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6468FA3F5F760D3FCAD6CDBF /* ContentView.swift in Sources */,
+ 45714AF6D1D12AF5C36694FB /* WakuExampleApp.swift in Sources */,
+ C4EA202B782038F96336401F /* WakuNode.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 36939122077C66DD94082311 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ DEVELOPMENT_TEAM = 2Q52K2W84K;
+ HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/WakuExample";
+ INFOPLIST_FILE = WakuExample/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/../../build/ios/iphonesimulator-arm64";
+ MACOSX_DEPLOYMENT_TARGET = 15.6;
+ OTHER_LDFLAGS = (
+ "-lc++",
+ "-force_load",
+ "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64/libwaku.a",
+ "-lsqlite3",
+ "-lz",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = org.waku.example;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "WakuExample/WakuExample-Bridging-Header.h";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 9BA833A09EEDB4B3FCCD8F8E /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.6;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ A59ABFB792FED8974231E5AC /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "DEBUG=1",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.6;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ AF5ADDAA865B1F6BD4E70A79 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ DEVELOPMENT_TEAM = 2Q52K2W84K;
+ HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/WakuExample";
+ INFOPLIST_FILE = WakuExample/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 18.6;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64";
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/../../build/ios/iphonesimulator-arm64";
+ MACOSX_DEPLOYMENT_TARGET = 15.6;
+ OTHER_LDFLAGS = (
+ "-lc++",
+ "-force_load",
+ "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64/libwaku.a",
+ "-lsqlite3",
+ "-lz",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = org.waku.example;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES;
+ SWIFT_OBJC_BRIDGING_HEADER = "WakuExample/WakuExample-Bridging-Header.h";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 757FA0123629BD63CB254113 /* Build configuration list for PBXNativeTarget "WakuExample" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ AF5ADDAA865B1F6BD4E70A79 /* Debug */,
+ 36939122077C66DD94082311 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ B3A4F48294254543E79767C4 /* Build configuration list for PBXProject "WakuExample" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ A59ABFB792FED8974231E5AC /* Debug */,
+ 9BA833A09EEDB4B3FCCD8F8E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 4FF82F0F4AF8E1E34728F150 /* Project object */;
+}
diff --git a/examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..919434a62
--- /dev/null
+++ b/examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/examples/ios/WakuExample/ContentView.swift b/examples/ios/WakuExample/ContentView.swift
new file mode 100644
index 000000000..14bb4ee1d
--- /dev/null
+++ b/examples/ios/WakuExample/ContentView.swift
@@ -0,0 +1,229 @@
+//
+// ContentView.swift
+// WakuExample
+//
+// Minimal chat PoC using libwaku on iOS
+//
+
+import SwiftUI
+
+struct ContentView: View {
+ @StateObject private var wakuNode = WakuNode()
+ @State private var messageText = ""
+
+ var body: some View {
+ ZStack {
+ // Main content
+ VStack(spacing: 0) {
+ // Header with status
+ HStack {
+ Circle()
+ .fill(statusColor)
+ .frame(width: 10, height: 10)
+ VStack(alignment: .leading, spacing: 2) {
+ Text(wakuNode.status.rawValue)
+ .font(.caption)
+ if wakuNode.status == .running {
+ HStack(spacing: 4) {
+ Text(wakuNode.isConnected ? "Connected" : "Discovering...")
+ Text("•")
+ filterStatusView
+ }
+ .font(.caption2)
+ .foregroundColor(.secondary)
+
+ // Subscription maintenance status
+ if wakuNode.subscriptionMaintenanceActive {
+ HStack(spacing: 4) {
+ Image(systemName: "arrow.triangle.2.circlepath")
+ .foregroundColor(.blue)
+ Text("Maintenance active")
+ if wakuNode.failedSubscribeAttempts > 0 {
+ Text("(\(wakuNode.failedSubscribeAttempts) retries)")
+ .foregroundColor(.orange)
+ }
+ }
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+ Spacer()
+ if wakuNode.status == .stopped {
+ Button("Start") {
+ wakuNode.start()
+ }
+ .buttonStyle(.borderedProminent)
+ .controlSize(.small)
+ } else if wakuNode.status == .running {
+ if !wakuNode.filterSubscribed {
+ Button("Resub") {
+ wakuNode.resubscribe()
+ }
+ .buttonStyle(.bordered)
+ .controlSize(.small)
+ }
+ Button("Stop") {
+ wakuNode.stop()
+ }
+ .buttonStyle(.bordered)
+ .controlSize(.small)
+ }
+ }
+ .padding()
+ .background(Color.gray.opacity(0.1))
+
+ // Messages list
+ ScrollViewReader { proxy in
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 8) {
+ ForEach(wakuNode.receivedMessages.reversed()) { message in
+ MessageBubble(message: message)
+ .id(message.id)
+ }
+ }
+ .padding()
+ }
+ .onChange(of: wakuNode.receivedMessages.count) { _, newCount in
+ if let lastMessage = wakuNode.receivedMessages.first {
+ withAnimation {
+ proxy.scrollTo(lastMessage.id, anchor: .bottom)
+ }
+ }
+ }
+ }
+
+ Divider()
+
+ // Message input
+ HStack(spacing: 12) {
+ TextField("Message", text: $messageText)
+ .textFieldStyle(.roundedBorder)
+ .disabled(wakuNode.status != .running)
+
+ Button(action: sendMessage) {
+ Image(systemName: "paperplane.fill")
+ .foregroundColor(.white)
+ .padding(10)
+ .background(canSend ? Color.blue : Color.gray)
+ .clipShape(Circle())
+ }
+ .disabled(!canSend)
+ }
+ .padding()
+ .background(Color.gray.opacity(0.1))
+ }
+
+ // Toast overlay for errors
+ VStack {
+ ForEach(wakuNode.errorQueue) { error in
+ ToastView(error: error) {
+ wakuNode.dismissError(error)
+ }
+ .transition(.asymmetric(
+ insertion: .move(edge: .top).combined(with: .opacity),
+ removal: .opacity
+ ))
+ }
+ Spacer()
+ }
+ .padding(.top, 8)
+ .animation(.easeInOut(duration: 0.3), value: wakuNode.errorQueue)
+ }
+ }
+
+ private var statusColor: Color {
+ switch wakuNode.status {
+ case .stopped: return .gray
+ case .starting: return .yellow
+ case .running: return .green
+ case .error: return .red
+ }
+ }
+
+ @ViewBuilder
+ private var filterStatusView: some View {
+ if wakuNode.filterSubscribed {
+ Text("Filter OK")
+ .foregroundColor(.green)
+ } else if wakuNode.failedSubscribeAttempts > 0 {
+ Text("Filter retrying (\(wakuNode.failedSubscribeAttempts))")
+ .foregroundColor(.orange)
+ } else {
+ Text("Filter pending")
+ .foregroundColor(.orange)
+ }
+ }
+
+ private var canSend: Bool {
+ wakuNode.status == .running && wakuNode.isConnected && !messageText.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+
+ private func sendMessage() {
+ let text = messageText.trimmingCharacters(in: .whitespaces)
+ guard !text.isEmpty else { return }
+
+ wakuNode.publish(message: text)
+ messageText = ""
+ }
+}
+
+// MARK: - Toast View
+
+struct ToastView: View {
+ let error: TimestampedError
+ let onDismiss: () -> Void
+
+ var body: some View {
+ HStack(spacing: 12) {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .foregroundColor(.white)
+
+ Text(error.message)
+ .font(.subheadline)
+ .foregroundColor(.white)
+ .lineLimit(2)
+
+ Spacer()
+
+ Button(action: onDismiss) {
+ Image(systemName: "xmark.circle.fill")
+ .foregroundColor(.white.opacity(0.8))
+ .font(.title3)
+ }
+ .buttonStyle(.plain)
+ }
+ .padding(.horizontal, 16)
+ .padding(.vertical, 12)
+ .background(
+ RoundedRectangle(cornerRadius: 12)
+ .fill(Color.red.opacity(0.9))
+ .shadow(color: .black.opacity(0.2), radius: 8, x: 0, y: 4)
+ )
+ .padding(.horizontal, 16)
+ .padding(.vertical, 4)
+ }
+}
+
+// MARK: - Message Bubble
+
+struct MessageBubble: View {
+ let message: WakuMessage
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(message.payload)
+ .padding(10)
+ .background(Color.blue.opacity(0.1))
+ .cornerRadius(12)
+
+ Text(message.timestamp, style: .time)
+ .font(.caption2)
+ .foregroundColor(.secondary)
+ }
+ }
+}
+
+#Preview {
+ ContentView()
+}
diff --git a/examples/ios/WakuExample/Info.plist b/examples/ios/WakuExample/Info.plist
new file mode 100644
index 000000000..a9222555a
--- /dev/null
+++ b/examples/ios/WakuExample/Info.plist
@@ -0,0 +1,36 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Waku Example
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ org.waku.example
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ WakuExample
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
+ UILaunchScreen
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+
+
+
diff --git a/examples/ios/WakuExample/WakuExample-Bridging-Header.h b/examples/ios/WakuExample/WakuExample-Bridging-Header.h
new file mode 100644
index 000000000..50595450e
--- /dev/null
+++ b/examples/ios/WakuExample/WakuExample-Bridging-Header.h
@@ -0,0 +1,15 @@
+//
+// WakuExample-Bridging-Header.h
+// WakuExample
+//
+// Bridging header to expose libwaku C functions to Swift
+//
+
+#ifndef WakuExample_Bridging_Header_h
+#define WakuExample_Bridging_Header_h
+
+#import "libwaku.h"
+
+#endif /* WakuExample_Bridging_Header_h */
+
+
diff --git a/examples/ios/WakuExample/WakuExampleApp.swift b/examples/ios/WakuExample/WakuExampleApp.swift
new file mode 100644
index 000000000..fb99785aa
--- /dev/null
+++ b/examples/ios/WakuExample/WakuExampleApp.swift
@@ -0,0 +1,19 @@
+//
+// WakuExampleApp.swift
+// WakuExample
+//
+// SwiftUI app entry point for Waku iOS example
+//
+
+import SwiftUI
+
+@main
+struct WakuExampleApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ContentView()
+ }
+ }
+}
+
+
diff --git a/examples/ios/WakuExample/WakuNode.swift b/examples/ios/WakuExample/WakuNode.swift
new file mode 100644
index 000000000..245529a2f
--- /dev/null
+++ b/examples/ios/WakuExample/WakuNode.swift
@@ -0,0 +1,739 @@
+//
+// WakuNode.swift
+// WakuExample
+//
+// Swift wrapper around libwaku C API for edge mode (lightpush + filter)
+// Uses Swift actors for thread safety and UI responsiveness
+//
+
+import Foundation
+
+// MARK: - Data Types
+
+/// Message received from Waku network
+struct WakuMessage: Identifiable, Equatable, Sendable {
+ let id: String // messageHash from Waku - unique identifier for deduplication
+ let payload: String
+ let contentTopic: String
+ let timestamp: Date
+}
+
+/// Waku node status
+enum WakuNodeStatus: String, Sendable {
+ case stopped = "Stopped"
+ case starting = "Starting..."
+ case running = "Running"
+ case error = "Error"
+}
+
+/// Status updates from WakuActor to WakuNode
+enum WakuStatusUpdate: Sendable {
+ case statusChanged(WakuNodeStatus)
+ case connectionChanged(isConnected: Bool)
+ case filterSubscriptionChanged(subscribed: Bool, failedAttempts: Int)
+ case maintenanceChanged(active: Bool)
+ case error(String)
+}
+
+/// Error with timestamp for toast queue
+struct TimestampedError: Identifiable, Equatable {
+ let id = UUID()
+ let message: String
+ let timestamp: Date
+
+ static func == (lhs: TimestampedError, rhs: TimestampedError) -> Bool {
+ lhs.id == rhs.id
+ }
+}
+
+// MARK: - Callback Context for C API
+
+private final class CallbackContext: @unchecked Sendable {
+ private let lock = NSLock()
+ private var _continuation: CheckedContinuation<(success: Bool, result: String?), Never>?
+ private var _resumed = false
+ var success: Bool = false
+ var result: String?
+
+ var continuation: CheckedContinuation<(success: Bool, result: String?), Never>? {
+ get {
+ lock.lock()
+ defer { lock.unlock() }
+ return _continuation
+ }
+ set {
+ lock.lock()
+ defer { lock.unlock() }
+ _continuation = newValue
+ }
+ }
+
+ /// Thread-safe resume - ensures continuation is only resumed once
+ /// Returns true if this call actually resumed, false if already resumed
+ @discardableResult
+ func resumeOnce(returning value: (success: Bool, result: String?)) -> Bool {
+ lock.lock()
+ defer { lock.unlock() }
+
+ guard !_resumed, let cont = _continuation else {
+ return false
+ }
+
+ _resumed = true
+ _continuation = nil
+ cont.resume(returning: value)
+ return true
+ }
+}
+
+// MARK: - WakuActor
+
+/// Actor that isolates all Waku operations from the main thread
+/// All C API calls and mutable state are contained here
+actor WakuActor {
+
+ // MARK: - State
+
+ private var ctx: UnsafeMutableRawPointer?
+ private var seenMessageHashes: Set = []
+ private var isSubscribed: Bool = false
+ private var isSubscribing: Bool = false
+ private var hasPeers: Bool = false
+ private var maintenanceTask: Task?
+ private var eventProcessingTask: Task?
+
+ // Stream continuations for communicating with UI
+ private var messageContinuation: AsyncStream.Continuation?
+ private var statusContinuation: AsyncStream.Continuation?
+
+ // Event stream from C callbacks
+ private var eventContinuation: AsyncStream.Continuation?
+
+ // Configuration
+ let defaultPubsubTopic = "/waku/2/rs/1/0"
+ let defaultContentTopic = "/waku-ios-example/1/chat/proto"
+ private let staticPeer = "/dns4/node-01.do-ams3.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ"
+
+ // Subscription maintenance settings
+ private let maxFailedSubscribes = 3
+ private let retryWaitSeconds: UInt64 = 2_000_000_000 // 2 seconds in nanoseconds
+ private let maintenanceIntervalSeconds: UInt64 = 30_000_000_000 // 30 seconds in nanoseconds
+ private let maxSeenHashes = 1000
+
+ // MARK: - Static callback storage (for C callbacks)
+
+ // We need a way for C callbacks to reach the actor
+ // Using a simple static reference (safe because we only have one instance)
+ private static var sharedEventContinuation: AsyncStream.Continuation?
+
+ private static let eventCallback: WakuCallBack = { ret, msg, len, userData in
+ guard ret == RET_OK, let msg = msg else { return }
+ let str = String(cString: msg)
+ WakuActor.sharedEventContinuation?.yield(str)
+ }
+
+ private static let syncCallback: WakuCallBack = { ret, msg, len, userData in
+ guard let userData = userData else { return }
+ let context = Unmanaged.fromOpaque(userData).takeUnretainedValue()
+ let success = (ret == RET_OK)
+ var resultStr: String? = nil
+ if let msg = msg {
+ resultStr = String(cString: msg)
+ }
+ context.resumeOnce(returning: (success, resultStr))
+ }
+
+ // MARK: - Stream Setup
+
+ func setMessageContinuation(_ continuation: AsyncStream.Continuation?) {
+ self.messageContinuation = continuation
+ }
+
+ func setStatusContinuation(_ continuation: AsyncStream.Continuation?) {
+ self.statusContinuation = continuation
+ }
+
+ // MARK: - Public API
+
+ var isRunning: Bool {
+ ctx != nil
+ }
+
+ var hasConnectedPeers: Bool {
+ hasPeers
+ }
+
+ func start() async {
+ guard ctx == nil else {
+ print("[WakuActor] Already started")
+ return
+ }
+
+ statusContinuation?.yield(.statusChanged(.starting))
+
+ // Create event stream for C callbacks
+ let eventStream = AsyncStream { continuation in
+ self.eventContinuation = continuation
+ WakuActor.sharedEventContinuation = continuation
+ }
+
+ // Start event processing task
+ eventProcessingTask = Task { [weak self] in
+ for await eventJson in eventStream {
+ await self?.handleEvent(eventJson)
+ }
+ }
+
+ // Initialize the node
+ let success = await initializeNode()
+
+ if success {
+ statusContinuation?.yield(.statusChanged(.running))
+
+ // Connect to peer
+ let connected = await connectToPeer()
+ if connected {
+ hasPeers = true
+ statusContinuation?.yield(.connectionChanged(isConnected: true))
+
+ // Start maintenance loop
+ startMaintenanceLoop()
+ } else {
+ statusContinuation?.yield(.error("Failed to connect to service peer"))
+ }
+ }
+ }
+
+ func stop() async {
+ guard let context = ctx else { return }
+
+ // Stop maintenance loop
+ maintenanceTask?.cancel()
+ maintenanceTask = nil
+
+ // Stop event processing
+ eventProcessingTask?.cancel()
+ eventProcessingTask = nil
+
+ // Close event stream
+ eventContinuation?.finish()
+ eventContinuation = nil
+ WakuActor.sharedEventContinuation = nil
+
+ statusContinuation?.yield(.statusChanged(.stopped))
+ statusContinuation?.yield(.connectionChanged(isConnected: false))
+ statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: 0))
+ statusContinuation?.yield(.maintenanceChanged(active: false))
+
+ // Reset state
+ let ctxToStop = context
+ ctx = nil
+ isSubscribed = false
+ isSubscribing = false
+ hasPeers = false
+ seenMessageHashes.removeAll()
+
+ // Unsubscribe and stop in background (fire and forget)
+ Task.detached {
+ // Unsubscribe
+ _ = await self.callWakuSync { waku_filter_unsubscribe_all(ctxToStop, WakuActor.syncCallback, $0) }
+ print("[WakuActor] Unsubscribed from filter")
+
+ // Stop
+ _ = await self.callWakuSync { waku_stop(ctxToStop, WakuActor.syncCallback, $0) }
+ print("[WakuActor] Node stopped")
+
+ // Destroy
+ _ = await self.callWakuSync { waku_destroy(ctxToStop, WakuActor.syncCallback, $0) }
+ print("[WakuActor] Node destroyed")
+ }
+ }
+
+ func publish(message: String, contentTopic: String? = nil) async {
+ guard let context = ctx else {
+ print("[WakuActor] Node not started")
+ return
+ }
+
+ guard hasPeers else {
+ print("[WakuActor] No peers connected yet")
+ statusContinuation?.yield(.error("No peers connected yet. Please wait..."))
+ return
+ }
+
+ let topic = contentTopic ?? defaultContentTopic
+ guard let payloadData = message.data(using: .utf8) else { return }
+ let payloadBase64 = payloadData.base64EncodedString()
+ let timestamp = Int64(Date().timeIntervalSince1970 * 1_000_000_000)
+ let jsonMessage = """
+ {"payload":"\(payloadBase64)","contentTopic":"\(topic)","timestamp":\(timestamp)}
+ """
+
+ let result = await callWakuSync { userData in
+ waku_lightpush_publish(
+ context,
+ self.defaultPubsubTopic,
+ jsonMessage,
+ WakuActor.syncCallback,
+ userData
+ )
+ }
+
+ if result.success {
+ print("[WakuActor] Published message")
+ } else {
+ print("[WakuActor] Publish error: \(result.result ?? "unknown")")
+ statusContinuation?.yield(.error("Failed to send message"))
+ }
+ }
+
+ func resubscribe() async {
+ print("[WakuActor] Force resubscribe requested")
+ isSubscribed = false
+ isSubscribing = false
+ statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: 0))
+ _ = await subscribe()
+ }
+
+ // MARK: - Private Methods
+
+ private func initializeNode() async -> Bool {
+ let config = """
+ {
+ "tcpPort": 60000,
+ "clusterId": 1,
+ "shards": [0],
+ "relay": false,
+ "lightpush": true,
+ "filter": true,
+ "logLevel": "DEBUG",
+ "discv5Discovery": true,
+ "discv5BootstrapNodes": [
+ "enr:-QESuEB4Dchgjn7gfAvwB00CxTA-nGiyk-aALI-H4dYSZD3rUk7bZHmP8d2U6xDiQ2vZffpo45Jp7zKNdnwDUx6g4o6XAYJpZIJ2NIJpcIRA4VDAim11bHRpYWRkcnO4XAArNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwAtNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOvD3S3jUNICsrOILlmhENiWAMmMVlAl6-Q8wRB7hidY4N0Y3CCdl-DdWRwgiMohXdha3UyDw",
+ "enr:-QEkuEBIkb8q8_mrorHndoXH9t5N6ZfD-jehQCrYeoJDPHqT0l0wyaONa2-piRQsi3oVKAzDShDVeoQhy0uwN1xbZfPZAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQKnGt-GSgqPSf3IAPM7bFgTlpczpMZZLF3geeoNNsxzSoN0Y3CCdl-DdWRwgiMohXdha3UyDw"
+ ],
+ "discv5UdpPort": 9999,
+ "dnsDiscovery": true,
+ "dnsDiscoveryUrl": "enrtree://AOGYWMBYOUIMOENHXCHILPKY3ZRFEULMFI4DOM442QSZ73TT2A7VI@test.waku.nodes.status.im",
+ "dnsDiscoveryNameServers": ["8.8.8.8", "1.0.0.1"]
+ }
+ """
+
+ // Create node - waku_new is special, it returns the context directly
+ let createResult = await withCheckedContinuation { (continuation: CheckedContinuation<(ctx: UnsafeMutableRawPointer?, success: Bool, result: String?), Never>) in
+ let callbackCtx = CallbackContext()
+ let userDataPtr = Unmanaged.passRetained(callbackCtx).toOpaque()
+
+ // Set up a simple callback for waku_new
+ let newCtx = waku_new(config, { ret, msg, len, userData in
+ guard let userData = userData else { return }
+ let context = Unmanaged.fromOpaque(userData).takeUnretainedValue()
+ context.success = (ret == RET_OK)
+ if let msg = msg {
+ context.result = String(cString: msg)
+ }
+ }, userDataPtr)
+
+ // Small delay to ensure callback completes
+ DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
+ Unmanaged.fromOpaque(userDataPtr).release()
+ continuation.resume(returning: (newCtx, callbackCtx.success, callbackCtx.result))
+ }
+ }
+
+ guard createResult.ctx != nil else {
+ statusContinuation?.yield(.statusChanged(.error))
+ statusContinuation?.yield(.error("Failed to create node: \(createResult.result ?? "unknown")"))
+ return false
+ }
+
+ ctx = createResult.ctx
+
+ // Set event callback
+ waku_set_event_callback(ctx, WakuActor.eventCallback, nil)
+
+ // Start node
+ let startResult = await callWakuSync { userData in
+ waku_start(self.ctx, WakuActor.syncCallback, userData)
+ }
+
+ guard startResult.success else {
+ statusContinuation?.yield(.statusChanged(.error))
+ statusContinuation?.yield(.error("Failed to start node: \(startResult.result ?? "unknown")"))
+ ctx = nil
+ return false
+ }
+
+ print("[WakuActor] Node started")
+ return true
+ }
+
+ private func connectToPeer() async -> Bool {
+ guard let context = ctx else { return false }
+
+ print("[WakuActor] Connecting to static peer...")
+
+ let result = await callWakuSync { userData in
+ waku_connect(context, self.staticPeer, 10000, WakuActor.syncCallback, userData)
+ }
+
+ if result.success {
+ print("[WakuActor] Connected to peer successfully")
+ return true
+ } else {
+ print("[WakuActor] Failed to connect: \(result.result ?? "unknown")")
+ return false
+ }
+ }
+
+ private func subscribe(contentTopic: String? = nil) async -> Bool {
+ guard let context = ctx else { return false }
+ guard !isSubscribed && !isSubscribing else { return isSubscribed }
+
+ isSubscribing = true
+ let topic = contentTopic ?? defaultContentTopic
+
+ let result = await callWakuSync { userData in
+ waku_filter_subscribe(
+ context,
+ self.defaultPubsubTopic,
+ topic,
+ WakuActor.syncCallback,
+ userData
+ )
+ }
+
+ isSubscribing = false
+
+ if result.success {
+ print("[WakuActor] Subscribe request successful to \(topic)")
+ isSubscribed = true
+ statusContinuation?.yield(.filterSubscriptionChanged(subscribed: true, failedAttempts: 0))
+ return true
+ } else {
+ print("[WakuActor] Subscribe error: \(result.result ?? "unknown")")
+ isSubscribed = false
+ return false
+ }
+ }
+
+ private func pingFilterPeer() async -> Bool {
+ guard let context = ctx else { return false }
+
+ let result = await callWakuSync { userData in
+ waku_ping_peer(
+ context,
+ self.staticPeer,
+ 10000,
+ WakuActor.syncCallback,
+ userData
+ )
+ }
+
+ return result.success
+ }
+
+ // MARK: - Subscription Maintenance
+
+ private func startMaintenanceLoop() {
+ guard maintenanceTask == nil else {
+ print("[WakuActor] Maintenance loop already running")
+ return
+ }
+
+ statusContinuation?.yield(.maintenanceChanged(active: true))
+ print("[WakuActor] Starting subscription maintenance loop")
+
+ maintenanceTask = Task { [weak self] in
+ guard let self = self else { return }
+
+ var failedSubscribes = 0
+ var isFirstPingOnConnection = true
+
+ while !Task.isCancelled {
+ guard await self.isRunning else { break }
+
+ print("[WakuActor] Maintaining subscription...")
+
+ let pingSuccess = await self.pingFilterPeer()
+ let currentlySubscribed = await self.isSubscribed
+
+ if pingSuccess && currentlySubscribed {
+ print("[WakuActor] Subscription is live, waiting 30s")
+ try? await Task.sleep(nanoseconds: self.maintenanceIntervalSeconds)
+ continue
+ }
+
+ if !isFirstPingOnConnection && !pingSuccess {
+ print("[WakuActor] Ping failed - subscription may be lost")
+ await self.statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: failedSubscribes))
+ }
+ isFirstPingOnConnection = false
+
+ print("[WakuActor] No active subscription found. Sending subscribe request...")
+
+ await self.resetSubscriptionState()
+ let subscribeSuccess = await self.subscribe()
+
+ if subscribeSuccess {
+ print("[WakuActor] Subscribe request successful")
+ failedSubscribes = 0
+ try? await Task.sleep(nanoseconds: self.maintenanceIntervalSeconds)
+ continue
+ }
+
+ failedSubscribes += 1
+ await self.statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: failedSubscribes))
+ print("[WakuActor] Subscribe request failed. Attempt \(failedSubscribes)/\(self.maxFailedSubscribes)")
+
+ if failedSubscribes < self.maxFailedSubscribes {
+ print("[WakuActor] Retrying in 2s...")
+ try? await Task.sleep(nanoseconds: self.retryWaitSeconds)
+ } else {
+ print("[WakuActor] Max subscribe failures reached")
+ await self.statusContinuation?.yield(.error("Filter subscription failed after \(self.maxFailedSubscribes) attempts"))
+ failedSubscribes = 0
+ try? await Task.sleep(nanoseconds: self.maintenanceIntervalSeconds)
+ }
+ }
+
+ print("[WakuActor] Subscription maintenance loop stopped")
+ await self.statusContinuation?.yield(.maintenanceChanged(active: false))
+ }
+ }
+
+ private func resetSubscriptionState() {
+ isSubscribed = false
+ isSubscribing = false
+ }
+
+ // MARK: - Event Handling
+
+ private func handleEvent(_ eventJson: String) {
+ guard let data = eventJson.data(using: .utf8),
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+ let eventType = json["eventType"] as? String else {
+ return
+ }
+
+ if eventType == "connection_change" {
+ handleConnectionChange(json)
+ } else if eventType == "message" {
+ handleMessage(json)
+ }
+ }
+
+ private func handleConnectionChange(_ json: [String: Any]) {
+ guard let peerEvent = json["peerEvent"] as? String else { return }
+
+ if peerEvent == "Joined" || peerEvent == "Identified" {
+ hasPeers = true
+ statusContinuation?.yield(.connectionChanged(isConnected: true))
+ } else if peerEvent == "Left" {
+ statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: 0))
+ }
+ }
+
+ private func handleMessage(_ json: [String: Any]) {
+ guard let messageHash = json["messageHash"] as? String,
+ let wakuMessage = json["wakuMessage"] as? [String: Any],
+ let payloadBase64 = wakuMessage["payload"] as? String,
+ let contentTopic = wakuMessage["contentTopic"] as? String,
+ let payloadData = Data(base64Encoded: payloadBase64),
+ let payloadString = String(data: payloadData, encoding: .utf8) else {
+ return
+ }
+
+ // Deduplicate
+ guard !seenMessageHashes.contains(messageHash) else {
+ return
+ }
+
+ seenMessageHashes.insert(messageHash)
+
+ // Limit memory usage
+ if seenMessageHashes.count > maxSeenHashes {
+ seenMessageHashes.removeAll()
+ }
+
+ let message = WakuMessage(
+ id: messageHash,
+ payload: payloadString,
+ contentTopic: contentTopic,
+ timestamp: Date()
+ )
+
+ messageContinuation?.yield(message)
+ }
+
+ // MARK: - Helper for synchronous C calls
+
+ private func callWakuSync(_ work: @escaping (UnsafeMutableRawPointer) -> Void) async -> (success: Bool, result: String?) {
+ await withCheckedContinuation { continuation in
+ let context = CallbackContext()
+ context.continuation = continuation
+ let userDataPtr = Unmanaged.passRetained(context).toOpaque()
+
+ work(userDataPtr)
+
+ // Set a timeout to avoid hanging forever
+ DispatchQueue.global().asyncAfter(deadline: .now() + 15) {
+ // Try to resume with timeout - will be ignored if callback already resumed
+ let didTimeout = context.resumeOnce(returning: (false, "Timeout"))
+ if didTimeout {
+ print("[WakuActor] Call timed out")
+ }
+ Unmanaged.fromOpaque(userDataPtr).release()
+ }
+ }
+ }
+}
+
+// MARK: - WakuNode (MainActor UI Wrapper)
+
+/// Main-thread UI wrapper that consumes updates from WakuActor via AsyncStreams
+@MainActor
+class WakuNode: ObservableObject {
+
+ // MARK: - Published Properties (UI State)
+
+ @Published var status: WakuNodeStatus = .stopped
+ @Published var receivedMessages: [WakuMessage] = []
+ @Published var errorQueue: [TimestampedError] = []
+ @Published var isConnected: Bool = false
+ @Published var filterSubscribed: Bool = false
+ @Published var subscriptionMaintenanceActive: Bool = false
+ @Published var failedSubscribeAttempts: Int = 0
+
+ // Topics (read-only access to actor's config)
+ var defaultPubsubTopic: String { "/waku/2/rs/1/0" }
+ var defaultContentTopic: String { "/waku-ios-example/1/chat/proto" }
+
+ // MARK: - Private Properties
+
+ private let actor = WakuActor()
+ private var messageTask: Task?
+ private var statusTask: Task?
+
+ // MARK: - Initialization
+
+ init() {}
+
+ deinit {
+ messageTask?.cancel()
+ statusTask?.cancel()
+ }
+
+ // MARK: - Public API
+
+ func start() {
+ guard status == .stopped || status == .error else {
+ print("[WakuNode] Already started or starting")
+ return
+ }
+
+ // Create message stream
+ let messageStream = AsyncStream { continuation in
+ Task {
+ await self.actor.setMessageContinuation(continuation)
+ }
+ }
+
+ // Create status stream
+ let statusStream = AsyncStream { continuation in
+ Task {
+ await self.actor.setStatusContinuation(continuation)
+ }
+ }
+
+ // Start consuming messages
+ messageTask = Task { @MainActor in
+ for await message in messageStream {
+ self.receivedMessages.insert(message, at: 0)
+ if self.receivedMessages.count > 100 {
+ self.receivedMessages.removeLast()
+ }
+ }
+ }
+
+ // Start consuming status updates
+ statusTask = Task { @MainActor in
+ for await update in statusStream {
+ self.handleStatusUpdate(update)
+ }
+ }
+
+ // Start the actor
+ Task {
+ await actor.start()
+ }
+ }
+
+ func stop() {
+ messageTask?.cancel()
+ messageTask = nil
+ statusTask?.cancel()
+ statusTask = nil
+
+ Task {
+ await actor.stop()
+ }
+
+ // Immediate UI update
+ status = .stopped
+ isConnected = false
+ filterSubscribed = false
+ subscriptionMaintenanceActive = false
+ failedSubscribeAttempts = 0
+ }
+
+ func publish(message: String, contentTopic: String? = nil) {
+ Task {
+ await actor.publish(message: message, contentTopic: contentTopic)
+ }
+ }
+
+ func resubscribe() {
+ Task {
+ await actor.resubscribe()
+ }
+ }
+
+ func dismissError(_ error: TimestampedError) {
+ errorQueue.removeAll { $0.id == error.id }
+ }
+
+ func dismissAllErrors() {
+ errorQueue.removeAll()
+ }
+
+ // MARK: - Private Methods
+
+ private func handleStatusUpdate(_ update: WakuStatusUpdate) {
+ switch update {
+ case .statusChanged(let newStatus):
+ status = newStatus
+
+ case .connectionChanged(let connected):
+ isConnected = connected
+
+ case .filterSubscriptionChanged(let subscribed, let attempts):
+ filterSubscribed = subscribed
+ failedSubscribeAttempts = attempts
+
+ case .maintenanceChanged(let active):
+ subscriptionMaintenanceActive = active
+
+ case .error(let message):
+ let error = TimestampedError(message: message, timestamp: Date())
+ errorQueue.append(error)
+
+ // Schedule auto-dismiss after 10 seconds
+ let errorId = error.id
+ Task { @MainActor in
+ try? await Task.sleep(nanoseconds: 10_000_000_000)
+ self.errorQueue.removeAll { $0.id == errorId }
+ }
+ }
+ }
+}
diff --git a/examples/ios/WakuExample/libwaku.h b/examples/ios/WakuExample/libwaku.h
new file mode 100644
index 000000000..b5d6c9bab
--- /dev/null
+++ b/examples/ios/WakuExample/libwaku.h
@@ -0,0 +1,253 @@
+
+// Generated manually and inspired by the one generated by the Nim Compiler.
+// In order to see the header file generated by Nim just run `make libwaku`
+// from the root repo folder and the header should be created in
+// nimcache/release/libwaku/libwaku.h
+#ifndef __libwaku__
+#define __libwaku__
+
+#include
+#include
+
+// The possible returned values for the functions that return int
+#define RET_OK 0
+#define RET_ERR 1
+#define RET_MISSING_CALLBACK 2
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void (*WakuCallBack) (int callerRet, const char* msg, size_t len, void* userData);
+
+// Creates a new instance of the waku node.
+// Sets up the waku node from the given configuration.
+// Returns a pointer to the Context needed by the rest of the API functions.
+void* waku_new(
+ const char* configJson,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_start(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_stop(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+// Destroys an instance of a waku node created with waku_new
+int waku_destroy(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_version(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+// Sets a callback that will be invoked whenever an event occurs.
+// It is crucial that the passed callback is fast, non-blocking and potentially thread-safe.
+void waku_set_event_callback(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_content_topic(void* ctx,
+ const char* appName,
+ unsigned int appVersion,
+ const char* contentTopicName,
+ const char* encoding,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_pubsub_topic(void* ctx,
+ const char* topicName,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_default_pubsub_topic(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_relay_publish(void* ctx,
+ const char* pubSubTopic,
+ const char* jsonWakuMessage,
+ unsigned int timeoutMs,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_lightpush_publish(void* ctx,
+ const char* pubSubTopic,
+ const char* jsonWakuMessage,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_relay_subscribe(void* ctx,
+ const char* pubSubTopic,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_relay_add_protected_shard(void* ctx,
+ int clusterId,
+ int shardId,
+ char* publicKey,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_relay_unsubscribe(void* ctx,
+ const char* pubSubTopic,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_filter_subscribe(void* ctx,
+ const char* pubSubTopic,
+ const char* contentTopics,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_filter_unsubscribe(void* ctx,
+ const char* pubSubTopic,
+ const char* contentTopics,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_filter_unsubscribe_all(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_relay_get_num_connected_peers(void* ctx,
+ const char* pubSubTopic,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_relay_get_connected_peers(void* ctx,
+ const char* pubSubTopic,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_relay_get_num_peers_in_mesh(void* ctx,
+ const char* pubSubTopic,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_relay_get_peers_in_mesh(void* ctx,
+ const char* pubSubTopic,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_store_query(void* ctx,
+ const char* jsonQuery,
+ const char* peerAddr,
+ int timeoutMs,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_connect(void* ctx,
+ const char* peerMultiAddr,
+ unsigned int timeoutMs,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_disconnect_peer_by_id(void* ctx,
+ const char* peerId,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_disconnect_all_peers(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_dial_peer(void* ctx,
+ const char* peerMultiAddr,
+ const char* protocol,
+ int timeoutMs,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_dial_peer_by_id(void* ctx,
+ const char* peerId,
+ const char* protocol,
+ int timeoutMs,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_get_peerids_from_peerstore(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_get_connected_peers_info(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_get_peerids_by_protocol(void* ctx,
+ const char* protocol,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_listen_addresses(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_get_connected_peers(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+// Returns a list of multiaddress given a url to a DNS discoverable ENR tree
+// Parameters
+// char* entTreeUrl: URL containing a discoverable ENR tree
+// char* nameDnsServer: The nameserver to resolve the ENR tree url.
+// int timeoutMs: Timeout value in milliseconds to execute the call.
+int waku_dns_discovery(void* ctx,
+ const char* entTreeUrl,
+ const char* nameDnsServer,
+ int timeoutMs,
+ WakuCallBack callback,
+ void* userData);
+
+// Updates the bootnode list used for discovering new peers via DiscoveryV5
+// bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]`
+int waku_discv5_update_bootnodes(void* ctx,
+ char* bootnodes,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_start_discv5(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_stop_discv5(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+// Retrieves the ENR information
+int waku_get_my_enr(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_get_my_peerid(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_get_metrics(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_peer_exchange_request(void* ctx,
+ int numPeers,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_ping_peer(void* ctx,
+ const char* peerAddr,
+ int timeoutMs,
+ WakuCallBack callback,
+ void* userData);
+
+int waku_is_online(void* ctx,
+ WakuCallBack callback,
+ void* userData);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __libwaku__ */
diff --git a/examples/ios/project.yml b/examples/ios/project.yml
new file mode 100644
index 000000000..9519e8b9e
--- /dev/null
+++ b/examples/ios/project.yml
@@ -0,0 +1,47 @@
+name: WakuExample
+options:
+ bundleIdPrefix: org.waku
+ deploymentTarget:
+ iOS: "14.0"
+ xcodeVersion: "15.0"
+
+settings:
+ SWIFT_VERSION: "5.0"
+ SUPPORTED_PLATFORMS: "iphoneos iphonesimulator"
+ SUPPORTS_MACCATALYST: "NO"
+
+targets:
+ WakuExample:
+ type: application
+ platform: iOS
+ supportedDestinations: [iOS]
+ sources:
+ - WakuExample
+ settings:
+ INFOPLIST_FILE: WakuExample/Info.plist
+ PRODUCT_BUNDLE_IDENTIFIER: org.waku.example
+ SWIFT_OBJC_BRIDGING_HEADER: WakuExample/WakuExample-Bridging-Header.h
+ HEADER_SEARCH_PATHS:
+ - "$(PROJECT_DIR)/WakuExample"
+ "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]":
+ - "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64"
+ "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]":
+ - "$(PROJECT_DIR)/../../build/ios/iphonesimulator-arm64"
+ OTHER_LDFLAGS:
+ - "-lc++"
+ - "-lwaku"
+ IPHONEOS_DEPLOYMENT_TARGET: "14.0"
+ info:
+ path: WakuExample/Info.plist
+ properties:
+ CFBundleName: WakuExample
+ CFBundleDisplayName: Waku Example
+ CFBundleIdentifier: org.waku.example
+ CFBundleVersion: "1"
+ CFBundleShortVersionString: "1.0"
+ UILaunchScreen: {}
+ UISupportedInterfaceOrientations:
+ - UIInterfaceOrientationPortrait
+ NSAppTransportSecurity:
+ NSAllowsArbitraryLoads: true
+
diff --git a/library/ios_bearssl_stubs.c b/library/ios_bearssl_stubs.c
new file mode 100644
index 000000000..a028cdf25
--- /dev/null
+++ b/library/ios_bearssl_stubs.c
@@ -0,0 +1,32 @@
+/**
+ * iOS stubs for BearSSL tools functions not normally included in the library.
+ * These are typically from the BearSSL tools/ directory which is for CLI tools.
+ */
+
+#include
+
+/* x509_noanchor context - simplified stub */
+typedef struct {
+ void *vtable;
+ void *inner;
+} x509_noanchor_context;
+
+/* Stub for x509_noanchor_init - used to skip anchor validation */
+void x509_noanchor_init(x509_noanchor_context *xwc, const void **inner) {
+ if (xwc && inner) {
+ xwc->inner = (void*)*inner;
+ xwc->vtable = NULL;
+ }
+}
+
+/* TAs (Trust Anchors) - empty array stub */
+/* This is typically defined by applications with their CA certificates */
+typedef struct {
+ void *dn;
+ size_t dn_len;
+ unsigned flags;
+ void *pkey;
+} br_x509_trust_anchor;
+
+const br_x509_trust_anchor TAs[1] = {{0}};
+const size_t TAs_NUM = 0;
diff --git a/library/ios_natpmp_stubs.c b/library/ios_natpmp_stubs.c
new file mode 100644
index 000000000..ef635db10
--- /dev/null
+++ b/library/ios_natpmp_stubs.c
@@ -0,0 +1,14 @@
+/**
+ * iOS stub for getgateway.c functions.
+ * iOS doesn't have net/route.h, so we provide a stub that returns failure.
+ * NAT-PMP functionality won't work but the library will link.
+ */
+
+#include
+#include
+
+/* getdefaultgateway - returns -1 (failure) on iOS */
+int getdefaultgateway(in_addr_t *addr) {
+ (void)addr; /* unused */
+ return -1; /* failure - not supported on iOS */
+}
diff --git a/waku.nimble b/waku.nimble
index 7bfdfab12..5c5c09763 100644
--- a/waku.nimble
+++ b/waku.nimble
@@ -213,3 +213,182 @@ task libWakuAndroid, "Build the mobile bindings for Android":
let srcDir = "./library"
let extraParams = "-d:chronicles_log_level=ERROR"
buildMobileAndroid srcDir, extraParams
+
+### Mobile iOS
+import std/sequtils
+
+proc buildMobileIOS(srcDir = ".", params = "") =
+ echo "Building iOS libwaku library"
+
+ let iosArch = getEnv("IOS_ARCH")
+ let iosSdk = getEnv("IOS_SDK")
+ let sdkPath = getEnv("IOS_SDK_PATH")
+
+ if sdkPath.len == 0:
+ quit "Error: IOS_SDK_PATH not set. Set it to the path of the iOS SDK"
+
+ # Use SDK name in path to differentiate device vs simulator
+ let outDir = "build/ios/" & iosSdk & "-" & iosArch
+ if not dirExists outDir:
+ mkDir outDir
+
+ var extra_params = params
+ for i in 2 ..< paramCount():
+ extra_params &= " " & paramStr(i)
+
+ let cpu = if iosArch == "arm64": "arm64" else: "amd64"
+
+ # The output static library
+ let nimcacheDir = outDir & "/nimcache"
+ let objDir = outDir & "/obj"
+ let vendorObjDir = outDir & "/vendor_obj"
+ let aFile = outDir & "/libwaku.a"
+
+ if not dirExists objDir:
+ mkDir objDir
+ if not dirExists vendorObjDir:
+ mkDir vendorObjDir
+
+ let clangBase = "clang -arch " & iosArch & " -isysroot " & sdkPath &
+ " -mios-version-min=18.0 -fembed-bitcode -fPIC -O2"
+
+ # Generate C sources from Nim (no linking)
+ exec "nim c" &
+ " --nimcache:" & nimcacheDir &
+ " --os:ios --cpu:" & cpu &
+ " --compileOnly:on" &
+ " --noMain --mm:refc" &
+ " --threads:on --opt:size --header" &
+ " -d:metrics -d:discv5_protocol_id=d5waku" &
+ " --nimMainPrefix:libwaku --skipParentCfg:on" &
+ " --cc:clang" &
+ " " & extra_params &
+ " " & srcDir & "/libwaku.nim"
+
+ # Compile vendor C libraries for iOS
+
+ # --- BearSSL ---
+ echo "Compiling BearSSL for iOS..."
+ let bearSslSrcDir = "./vendor/nim-bearssl/bearssl/csources/src"
+ let bearSslIncDir = "./vendor/nim-bearssl/bearssl/csources/inc"
+ for path in walkDirRec(bearSslSrcDir):
+ if path.endsWith(".c"):
+ let relPath = path.replace(bearSslSrcDir & "/", "").replace("/", "_")
+ let baseName = relPath.changeFileExt("o")
+ let oFile = vendorObjDir / ("bearssl_" & baseName)
+ if not fileExists(oFile):
+ exec clangBase & " -I" & bearSslIncDir & " -I" & bearSslSrcDir & " -c " & path & " -o " & oFile
+
+ # --- secp256k1 ---
+ echo "Compiling secp256k1 for iOS..."
+ let secp256k1Dir = "./vendor/nim-secp256k1/vendor/secp256k1"
+ let secp256k1Flags = " -I" & secp256k1Dir & "/include" &
+ " -I" & secp256k1Dir & "/src" &
+ " -I" & secp256k1Dir &
+ " -DENABLE_MODULE_RECOVERY=1" &
+ " -DENABLE_MODULE_ECDH=1" &
+ " -DECMULT_WINDOW_SIZE=15" &
+ " -DECMULT_GEN_PREC_BITS=4"
+
+ # Main secp256k1 source
+ let secp256k1Obj = vendorObjDir / "secp256k1.o"
+ if not fileExists(secp256k1Obj):
+ exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/secp256k1.c -o " & secp256k1Obj
+
+ # Precomputed tables (required for ecmult operations)
+ let secp256k1PreEcmultObj = vendorObjDir / "secp256k1_precomputed_ecmult.o"
+ if not fileExists(secp256k1PreEcmultObj):
+ exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/precomputed_ecmult.c -o " & secp256k1PreEcmultObj
+
+ let secp256k1PreEcmultGenObj = vendorObjDir / "secp256k1_precomputed_ecmult_gen.o"
+ if not fileExists(secp256k1PreEcmultGenObj):
+ exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/precomputed_ecmult_gen.c -o " & secp256k1PreEcmultGenObj
+
+ # --- miniupnpc ---
+ echo "Compiling miniupnpc for iOS..."
+ let miniupnpcSrcDir = "./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/src"
+ let miniupnpcIncDir = "./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/include"
+ let miniupnpcBuildDir = "./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/build"
+ let miniupnpcFiles = @[
+ "addr_is_reserved.c", "connecthostport.c", "igd_desc_parse.c",
+ "minisoap.c", "minissdpc.c", "miniupnpc.c", "miniwget.c",
+ "minixml.c", "portlistingparse.c", "receivedata.c", "upnpcommands.c",
+ "upnpdev.c", "upnperrors.c", "upnpreplyparse.c"
+ ]
+ for fileName in miniupnpcFiles:
+ let srcPath = miniupnpcSrcDir / fileName
+ let oFile = vendorObjDir / ("miniupnpc_" & fileName.changeFileExt("o"))
+ if fileExists(srcPath) and not fileExists(oFile):
+ exec clangBase &
+ " -I" & miniupnpcIncDir &
+ " -I" & miniupnpcSrcDir &
+ " -I" & miniupnpcBuildDir &
+ " -DMINIUPNPC_SET_SOCKET_TIMEOUT" &
+ " -D_BSD_SOURCE -D_DEFAULT_SOURCE" &
+ " -c " & srcPath & " -o " & oFile
+
+ # --- libnatpmp ---
+ echo "Compiling libnatpmp for iOS..."
+ let natpmpSrcDir = "./vendor/nim-nat-traversal/vendor/libnatpmp-upstream"
+ # Only compile natpmp.c - getgateway.c uses net/route.h which is not available on iOS
+ let natpmpObj = vendorObjDir / "natpmp_natpmp.o"
+ if not fileExists(natpmpObj):
+ exec clangBase &
+ " -I" & natpmpSrcDir &
+ " -DENABLE_STRNATPMPERR" &
+ " -c " & natpmpSrcDir & "/natpmp.c -o " & natpmpObj
+
+ # Use iOS-specific stub for getgateway
+ let getgatewayStubSrc = "./library/ios_natpmp_stubs.c"
+ let getgatewayStubObj = vendorObjDir / "natpmp_getgateway_stub.o"
+ if fileExists(getgatewayStubSrc) and not fileExists(getgatewayStubObj):
+ exec clangBase & " -c " & getgatewayStubSrc & " -o " & getgatewayStubObj
+
+ # --- BearSSL stubs (for tools functions not in main library) ---
+ echo "Compiling BearSSL stubs for iOS..."
+ let bearSslStubsSrc = "./library/ios_bearssl_stubs.c"
+ let bearSslStubsObj = vendorObjDir / "bearssl_stubs.o"
+ if fileExists(bearSslStubsSrc) and not fileExists(bearSslStubsObj):
+ exec clangBase & " -c " & bearSslStubsSrc & " -o " & bearSslStubsObj
+
+ # Compile all Nim-generated C files to object files
+ echo "Compiling Nim-generated C files for iOS..."
+ var cFiles: seq[string] = @[]
+ for kind, path in walkDir(nimcacheDir):
+ if kind == pcFile and path.endsWith(".c"):
+ cFiles.add(path)
+
+ for cFile in cFiles:
+ let baseName = extractFilename(cFile).changeFileExt("o")
+ let oFile = objDir / baseName
+ exec clangBase &
+ " -DENABLE_STRNATPMPERR" &
+ " -I./vendor/nimbus-build-system/vendor/Nim/lib/" &
+ " -I./vendor/nim-bearssl/bearssl/csources/inc/" &
+ " -I./vendor/nim-bearssl/bearssl/csources/tools/" &
+ " -I./vendor/nim-bearssl/bearssl/abi/" &
+ " -I./vendor/nim-secp256k1/vendor/secp256k1/include/" &
+ " -I./vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/include/" &
+ " -I./vendor/nim-nat-traversal/vendor/libnatpmp-upstream/" &
+ " -I" & nimcacheDir &
+ " -c " & cFile &
+ " -o " & oFile
+
+ # Create static library from all object files
+ echo "Creating static library..."
+ var objFiles: seq[string] = @[]
+ for kind, path in walkDir(objDir):
+ if kind == pcFile and path.endsWith(".o"):
+ objFiles.add(path)
+ for kind, path in walkDir(vendorObjDir):
+ if kind == pcFile and path.endsWith(".o"):
+ objFiles.add(path)
+
+ exec "libtool -static -o " & aFile & " " & objFiles.join(" ")
+
+ echo "✔ iOS library created: " & aFile
+
+task libWakuIOS, "Build the mobile bindings for iOS":
+ let srcDir = "./library"
+ let extraParams = "-d:chronicles_log_level=ERROR"
+ buildMobileIOS srcDir, extraParams