diff --git a/internal/platform/implementation/apple/Mediums/BUILD b/internal/platform/implementation/apple/Mediums/BUILD index 4d84ac4d..d549b7a8 100644 --- a/internal/platform/implementation/apple/Mediums/BUILD +++ b/internal/platform/implementation/apple/Mediums/BUILD @@ -28,6 +28,7 @@ objc_library( "WifiLan/GNCMBonjourConnection.m", "WifiLan/GNCMBonjourService.m", "WifiLan/GNCMBonjourUtils.m", + "WifiLanV2/GNCWiFiLANSocket.m", ], hdrs = [ "Ble/GNCMBleCentral.h", @@ -40,6 +41,7 @@ objc_library( "WifiLan/GNCMBonjourConnection.h", "WifiLan/GNCMBonjourService.h", "WifiLan/GNCMBonjourUtils.h", + "WifiLanV2/GNCWiFiLANSocket.h", ], deps = [ "//internal/platform/implementation/apple:Shared", @@ -49,7 +51,7 @@ objc_library( "//proto/mediums:ble_frames_cc_proto", "//third_party/apple_frameworks:CoreBluetooth", "//third_party/apple_frameworks:Foundation", + "//third_party/apple_frameworks:Network", "//third_party/objective_c/google_toolbox_for_mac:GTM_Logger", - "@com_google_absl//absl/numeric:int128", ], ) diff --git a/internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANSocket.h b/internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANSocket.h new file mode 100644 index 00000000..36a36c2e --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANSocket.h @@ -0,0 +1,64 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import + +@interface GNCWiFiLANSocket : NSObject + +/** + * @remark init is not an available initializer. + */ +- (nonnull instancetype)init NS_UNAVAILABLE; + +/** + * Creates a socket that allows reading/writing for a given connection. + * + * @param connection A bidirectional data connection between a local and remote endpoint. It is the + * responsibility of the caller to maintain a strong reference to the connection + * object. + */ +- (nonnull instancetype)initWithConnection:(nonnull nw_connection_t)connection + NS_DESIGNATED_INITIALIZER; + +/** + * Reads the requested amount of bytes from the connection. + * + * Blocks execution until the bytes have been read or an error occurs. + * + * @param length The number of bytes to read. + * @param[out] error Error that will be populated on failure. A read may return non-nil data along + * with an error. This normally happens if the data read is shorter than the + * requested length. + */ +- (nullable NSData *)readMaxLength:(NSUInteger)length error:(NSError **_Nullable)error; + +/** + * Writes the given data to the connection. + * + * Blocks execution until the bytes have been successfully written or an error occurs. + * + * @param data The data to write. + * @param[out] error Error that will be populated on failure. + */ +- (BOOL)write:(NSData *)data error:(NSError **_Nullable)error; + +/** + * Gracefully closes the connection to remote endpoint. + * + * If a read or write is in progress, they will be unblocked with an error. + */ +- (void)close; + +@end diff --git a/internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANSocket.m b/internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANSocket.m new file mode 100644 index 00000000..e3ba102f --- /dev/null +++ b/internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANSocket.m @@ -0,0 +1,124 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "internal/platform/implementation/apple/Mediums/WifiLanV2/GNCWiFiLANSocket.h" + +#import +#import + +#import "GoogleToolboxForMac/GTMLogger.h" + +@interface GNCWiFiLANSocket () + +@property(nonatomic, weak, readonly) nw_connection_t connection; + +@end + +@implementation GNCWiFiLANSocket { +} + +- (instancetype)initWithConnection:(nw_connection_t)connection { + self = [super init]; + if (self) { + _connection = connection; + } + return self; +} + +- (NSData *)readMaxLength:(NSUInteger)length error:(NSError **)error { + __strong nw_connection_t connection = self.connection; + if (connection == nil) { + return nil; + } + + NSCondition *condition = [[NSCondition alloc] init]; + [condition lock]; + + __block NSData *blockResult = nil; + __block NSError *blockError = nil; + + nw_connection_receive( + connection, length, length, + ^(dispatch_data_t content, nw_content_context_t context, bool is_complete, nw_error_t error) { + [condition lock]; + if (error != nil) { + blockError = (__bridge_transfer NSError *)nw_error_copy_cf_error(error); + } +#if __LP64__ + // This cast is only safe in a 64-bit runtime. + blockResult = (NSData *)content; +#else + blockResult = nil; +#endif + [condition signal]; + [condition unlock]; + }); + + [condition wait]; + [condition unlock]; + + if (error != nil) { + *error = blockError; + } + return blockResult; +} + +- (BOOL)write:(NSData *)data error:(NSError **)error { + __strong nw_connection_t connection = self.connection; + if (connection == nil) { + return NO; + } + + NSCondition *condition = [[NSCondition alloc] init]; + [condition lock]; + + __block BOOL blockResult = NO; + __block NSError *blockError = nil; + + dispatch_data_t dispatchData = + dispatch_data_create(data.bytes, data.length, dispatch_get_main_queue(), + ^{ + // Since we block until the send is complete, `data` should stay + // alive when needed and released at the end of this method. + }); + + nw_connection_send(connection, dispatchData, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, false, + ^(nw_error_t error) { + [condition lock]; + blockResult = error == nil; + if (error != nil) { + blockError = (__bridge_transfer NSError *)nw_error_copy_cf_error(error); + } + [condition signal]; + [condition unlock]; + }); + + [condition wait]; + [condition unlock]; + + if (error != nil) { + *error = blockError; + } + return blockResult; +} + +- (void)close { + __strong nw_connection_t connection = self.connection; + if (connection == nil) { + return; + } + nw_connection_cancel(connection); +} + +@end