feat: compilation for iOS WIP (#3668)

* feat: compilation for iOS WIP

* fix: nim ios version 18
This commit is contained in:
Pablo Lopez 2025-12-22 15:40:09 +02:00 committed by GitHub
parent e3dd6203ae
commit 96196ab8bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1956 additions and 0 deletions

10
.gitignore vendored
View File

@ -59,6 +59,10 @@ nimbus-build-system.paths
/examples/nodejs/build/ /examples/nodejs/build/
/examples/rust/target/ /examples/rust/target/
# Xcode user data
xcuserdata/
*.xcuserstate
# Coverage # Coverage
coverage_html_report/ coverage_html_report/
@ -79,3 +83,9 @@ waku_handler.moc.cpp
# Nix build result # Nix build result
result result
# llms
AGENTS.md
nimble.develop
nimble.paths
nimbledeps

View File

@ -517,6 +517,51 @@ libwaku-android:
# It's likely this architecture is not used so we might just not support it. # It's likely this architecture is not used so we might just not support it.
# $(MAKE) libwaku-android-arm # $(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 cwaku_example: | build libwaku
echo -e $(BUILD_MSG) "build/$@" && \ echo -e $(BUILD_MSG) "build/$@" && \
cc -o "build/$@" \ cc -o "build/$@" \

View File

@ -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 = "<group>"; };
31BE20DB2755A11000723420 /* libwaku.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libwaku.h; sourceTree = "<group>"; };
5C5AAC91E0166D28BFA986DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
638A565C495A63CFF7396FBC /* WakuNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WakuNode.swift; sourceTree = "<group>"; };
7D8744E36DADC11F38A1CC99 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
A8655016B3DF9B0877631CE5 /* WakuExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WakuExample-Bridging-Header.h"; sourceTree = "<group>"; };
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 = "<group>";
};
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 = "<group>";
};
D40CD2446F177CAABB0A747A = {
isa = PBXGroup;
children = (
4F76CB85EC44E951B8E75522 /* WakuExample */,
34547A6259485BD047D6375C /* Products */,
);
sourceTree = "<group>";
};
/* 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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -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()
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Waku Example</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>org.waku.example</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>WakuExample</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UILaunchScreen</key>
<dict/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
</dict>
</plist>

View File

@ -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 */

View File

@ -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()
}
}
}

View File

@ -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<String> = []
private var isSubscribed: Bool = false
private var isSubscribing: Bool = false
private var hasPeers: Bool = false
private var maintenanceTask: Task<Void, Never>?
private var eventProcessingTask: Task<Void, Never>?
// Stream continuations for communicating with UI
private var messageContinuation: AsyncStream<WakuMessage>.Continuation?
private var statusContinuation: AsyncStream<WakuStatusUpdate>.Continuation?
// Event stream from C callbacks
private var eventContinuation: AsyncStream<String>.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<String>.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<CallbackContext>.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<WakuMessage>.Continuation?) {
self.messageContinuation = continuation
}
func setStatusContinuation(_ continuation: AsyncStream<WakuStatusUpdate>.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<String> { 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<CallbackContext>.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<CallbackContext>.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<CallbackContext>.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<Void, Never>?
private var statusTask: Task<Void, Never>?
// 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<WakuMessage> { continuation in
Task {
await self.actor.setMessageContinuation(continuation)
}
}
// Create status stream
let statusStream = AsyncStream<WakuStatusUpdate> { 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 }
}
}
}
}

View File

@ -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 <stddef.h>
#include <stdint.h>
// 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__ */

47
examples/ios/project.yml Normal file
View File

@ -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

View File

@ -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 <stddef.h>
/* 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;

View File

@ -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 <stdint.h>
#include <netinet/in.h>
/* getdefaultgateway - returns -1 (failure) on iOS */
int getdefaultgateway(in_addr_t *addr) {
(void)addr; /* unused */
return -1; /* failure - not supported on iOS */
}

View File

@ -213,3 +213,182 @@ task libWakuAndroid, "Build the mobile bindings for Android":
let srcDir = "./library" let srcDir = "./library"
let extraParams = "-d:chronicles_log_level=ERROR" let extraParams = "-d:chronicles_log_level=ERROR"
buildMobileAndroid srcDir, extraParams 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