mirror of
https://github.com/logos-messaging/logos-messaging-nim.git
synced 2026-01-02 05:53:11 +00:00
feat: compilation for iOS WIP (#3668)
* feat: compilation for iOS WIP * fix: nim ios version 18
This commit is contained in:
parent
e3dd6203ae
commit
96196ab8bc
10
.gitignore
vendored
10
.gitignore
vendored
@ -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
|
||||
|
||||
45
Makefile
45
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/$@" \
|
||||
|
||||
331
examples/ios/WakuExample.xcodeproj/project.pbxproj
Normal file
331
examples/ios/WakuExample.xcodeproj/project.pbxproj
Normal 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 */;
|
||||
}
|
||||
7
examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
229
examples/ios/WakuExample/ContentView.swift
Normal file
229
examples/ios/WakuExample/ContentView.swift
Normal 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()
|
||||
}
|
||||
36
examples/ios/WakuExample/Info.plist
Normal file
36
examples/ios/WakuExample/Info.plist
Normal 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>
|
||||
|
||||
15
examples/ios/WakuExample/WakuExample-Bridging-Header.h
Normal file
15
examples/ios/WakuExample/WakuExample-Bridging-Header.h
Normal 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 */
|
||||
|
||||
|
||||
19
examples/ios/WakuExample/WakuExampleApp.swift
Normal file
19
examples/ios/WakuExample/WakuExampleApp.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
739
examples/ios/WakuExample/WakuNode.swift
Normal file
739
examples/ios/WakuExample/WakuNode.swift
Normal 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
253
examples/ios/WakuExample/libwaku.h
Normal file
253
examples/ios/WakuExample/libwaku.h
Normal 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
47
examples/ios/project.yml
Normal 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
|
||||
|
||||
32
library/ios_bearssl_stubs.c
Normal file
32
library/ios_bearssl_stubs.c
Normal 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;
|
||||
14
library/ios_natpmp_stubs.c
Normal file
14
library/ios_natpmp_stubs.c
Normal 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 */
|
||||
}
|
||||
179
waku.nimble
179
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user