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/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
|
||||||
|
|||||||
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.
|
# 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/$@" \
|
||||||
|
|||||||
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 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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user