From 95880e4d953e5731d27d0ca3003952d9b87eafb0 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Wed, 9 Oct 2024 17:18:29 -0700 Subject: [PATCH] Support file download --- .../native_cupertino_bindings.m | 9 + .../Sources/cupertino_http/utils.h | 19 ++ .../Sources/cupertino_http/utils.m | 13 + .../url_session_delegate_test.dart | 2 +- pkgs/cupertino_http/ffigen.yaml | 5 +- .../cupertino_http/lib/src/cupertino_api.dart | 29 +- .../lib/src/native_cupertino_bindings.dart | 277 ++++++++++++++++-- 7 files changed, 318 insertions(+), 36 deletions(-) create mode 100644 pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/utils.h create mode 100644 pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/utils.m diff --git a/pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/native_cupertino_bindings.m b/pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/native_cupertino_bindings.m index 64a971f73d..8a0b906c78 100644 --- a/pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/native_cupertino_bindings.m +++ b/pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/native_cupertino_bindings.m @@ -13,6 +13,7 @@ #import #import #import +#import "utils.h" #if !__has_feature(objc_arc) #error "This file must be compiled with ARC enabled" @@ -292,3 +293,11 @@ _ListenerTrampoline33 _wrapListenerBlock_vzqe8w(_ListenerTrampoline33 block) NS_ block(arg0, arg1); }; } + +typedef void (^_ListenerTrampoline34)(id arg0, id arg1, id arg2, id arg3); +_ListenerTrampoline34 _wrapListenerBlock_19b8ge5(_ListenerTrampoline34 block) NS_RETURNS_RETAINED { + return ^void(id arg0, id arg1, id arg2, id arg3) { + objc_retainBlock(block); + block(objc_retain(arg0), objc_retain(arg1), objc_retain(arg2), objc_retain(arg3)); + }; +} diff --git a/pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/utils.h b/pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/utils.h new file mode 100644 index 0000000000..3dd8b2ae02 --- /dev/null +++ b/pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/utils.h @@ -0,0 +1,19 @@ +#import +#import +#import +#include + +#if !__has_feature(objc_arc) +#error "This file must be compiled with ARC enabled" +#endif + +typedef void (^_DidFinish)(void *, NSURLSession *session, + NSURLSessionDownloadTask *downloadTask, + NSURL *location); +typedef void (^_DidFinishWithLock)(NSCondition *lock, NSURLSession *session, + NSURLSessionDownloadTask *downloadTask, + NSURL *location); +/// Create a block useable as a +/// `URLSession:downloadTask:didFinishDownloadingToURL:` that can be used to +/// make an async Dart callback behave synchronously. +_DidFinish adaptFinishWithLock(_DidFinishWithLock block); diff --git a/pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/utils.m b/pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/utils.m new file mode 100644 index 0000000000..b4c005719f --- /dev/null +++ b/pkgs/cupertino_http/darwin/cupertino_http/Sources/cupertino_http/utils.m @@ -0,0 +1,13 @@ +#import "utils.h" + +_DidFinish adaptFinishWithLock(_DidFinishWithLock block) { + return ^void(void *, NSURLSession *session, + NSURLSessionDownloadTask *downloadTask, NSURL *location) { + NSCondition *lock = [[NSCondition alloc] init]; + + [lock lock]; + block(lock, session, downloadTask, location); + [lock lock]; + [lock unlock]; + }; +} diff --git a/pkgs/cupertino_http/example/integration_test/url_session_delegate_test.dart b/pkgs/cupertino_http/example/integration_test/url_session_delegate_test.dart index c08fbe9126..a636098896 100644 --- a/pkgs/cupertino_http/example/integration_test/url_session_delegate_test.dart +++ b/pkgs/cupertino_http/example/integration_test/url_session_delegate_test.dart @@ -232,7 +232,7 @@ void testOnFinishedDownloading(URLSessionConfiguration Function() config) { expect(actualContent, 'Hello World'); session.finishTasksAndInvalidate(); }); - }, skip: 'XXX will not work with current approach'); + }); } void testOnRedirect(URLSessionConfiguration Function() config) { diff --git a/pkgs/cupertino_http/ffigen.yaml b/pkgs/cupertino_http/ffigen.yaml index c5c5e4f7e0..b8e0027701 100644 --- a/pkgs/cupertino_http/ffigen.yaml +++ b/pkgs/cupertino_http/ffigen.yaml @@ -24,10 +24,7 @@ headers: - '/System/Volumes/Data/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Versions/C/Headers/NSOperation.h' - '/System/Volumes/Data/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Versions/C/Headers/NSError.h' - '/System/Volumes/Data/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Versions/C/Headers/NSDictionary.h' -# - 'src/CUPHTTPClientDelegate.h' -# - 'src/CUPHTTPForwardedDelegate.h' -# - 'src/CUPHTTPCompletionHelper.h' - - 'darwin/cupertino_http/Sources/cupertino_http/CUPHTTPStreamToNSInputStreamAdapter.h' + - 'darwin/cupertino_http/Sources/cupertino_http/utils.h' preamble: | // ignore_for_file: always_specify_types // ignore_for_file: camel_case_types diff --git a/pkgs/cupertino_http/lib/src/cupertino_api.dart b/pkgs/cupertino_http/lib/src/cupertino_api.dart index 36fdeb489f..771a245475 100644 --- a/pkgs/cupertino_http/lib/src/cupertino_api.dart +++ b/pkgs/cupertino_http/lib/src/cupertino_api.dart @@ -911,15 +911,28 @@ class URLSession extends _ObjectHolder { } if (onFinishedDownloading != null) { - ncb.NSURLSessionDownloadDelegate.addToBuilderAsListener(protoBuilder, - URLSession_downloadTask_didFinishDownloadingToURL_: - (nsSession, nsTask, nsUrl) { - onFinishedDownloading( - URLSession._(nsSession, - isBackground: isBackground, hasDelegate: true), - URLSessionDownloadTask._(nsTask), - nsurlToUri(nsUrl)); + final asyncFinishDownloading = + ncb.ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL + .listener((nsCondition, nsSession, nsTask, nsUrl) { + try { + onFinishedDownloading( + URLSession._(nsSession, + isBackground: isBackground, hasDelegate: true), + URLSessionDownloadTask._(nsTask), + nsurlToUri(nsUrl)); + } finally { + nsCondition.unlock(); + } }); + + final downloadDelegate = objc.getProtocol('NSURLSessionDownloadDelegate'); + final didFinishDownloadingToURL = objc + .registerName('URLSession:downloadTask:didFinishDownloadingToURL:'); + final signature = objc.getProtocolMethodSignature( + downloadDelegate, didFinishDownloadingToURL, + isRequired: true, isInstanceMethod: true); + protoBuilder.implementMethod(didFinishDownloadingToURL, signature, + linkedLibs.adaptFinishWithLock(asyncFinishDownloading)); } if (onWebSocketTaskOpened != null) { diff --git a/pkgs/cupertino_http/lib/src/native_cupertino_bindings.dart b/pkgs/cupertino_http/lib/src/native_cupertino_bindings.dart index 7ca8429f5a..0705f87ecc 100644 --- a/pkgs/cupertino_http/lib/src/native_cupertino_bindings.dart +++ b/pkgs/cupertino_http/lib/src/native_cupertino_bindings.dart @@ -40413,6 +40413,27 @@ class NativeCupertinoHttp { .release(); _NSFilePathErrorKey.value = value.ref.retainAndReturnPointer(); } + + /// Create a block useable as a + /// `URLSession:downloadTask:didFinishDownloadingToURL:` that can be used to + /// make an async Dart callback behave synchronously. + Dart_DidFinish adaptFinishWithLock( + Dart_DidFinishWithLock block, + ) { + return ObjCBlock_ffiVoid_ffiVoid_NSURLSession_NSURLSessionDownloadTask_NSURL + .castFromPointer( + _adaptFinishWithLock( + block.ref.pointer, + ), + retain: true, + release: true); + } + + late final _adaptFinishWithLockPtr = + _lookup>( + 'adaptFinishWithLock'); + late final _adaptFinishWithLock = _adaptFinishWithLockPtr + .asFunction<_DidFinish Function(_DidFinishWithLock)>(); } @ffi.Native< @@ -40653,6 +40674,13 @@ external ffi.Pointer _wrapListenerBlock_vzqe8w( ffi.Pointer block, ); +@ffi.Native< + ffi.Pointer Function( + ffi.Pointer)>(isLeaf: true) +external ffi.Pointer _wrapListenerBlock_19b8ge5( + ffi.Pointer block, +); + final class __mbstate_t extends ffi.Union { @ffi.Array.multi([128]) external ffi.Array __mbstate8; @@ -60288,12 +60316,7 @@ class NSURLSession extends objc.NSObject { return NSURLSession.castFromPointer(_ret, retain: false, release: true); } - /// data task convenience methods. These methods create tasks that - /// bypass the normal delegate calls for response and data delivery, - /// and provide a simple cancelable asynchronous interface to receiving - /// data. Errors will be returned in the NSURLErrorDomain, - /// see . The delegate, if any, will still be - /// called for authentication challenges. + /// dataTaskWithRequest:completionHandler: NSURLSessionDataTask dataTaskWithRequest_completionHandler_( NSURLRequest request, objc.ObjCBlock< @@ -60323,7 +60346,7 @@ class NSURLSession extends objc.NSObject { retain: true, release: true); } - /// upload convenience method. + /// uploadTaskWithRequest:fromFile:completionHandler: NSURLSessionUploadTask uploadTaskWithRequest_fromFile_completionHandler_( NSURLRequest request, objc.NSURL fileURL, @@ -60357,12 +60380,7 @@ class NSURLSession extends objc.NSObject { retain: true, release: true); } - /// Creates a URLSessionUploadTask from a resume data blob. If resuming from an upload - /// file, the file must still exist and be unmodified. - /// - /// - Parameter resumeData: Resume data blob from an incomplete upload, such as data returned by the cancelByProducingResumeData: method. - /// - Parameter completionHandler: The completion handler to call when the load request is complete. - /// - Returns: A new session upload task, or nil if the resumeData is invalid. + /// uploadTaskWithResumeData:completionHandler: NSURLSessionUploadTask uploadTaskWithResumeData_completionHandler_( objc.NSData resumeData, objc.ObjCBlock< @@ -60377,10 +60395,7 @@ class NSURLSession extends objc.NSObject { retain: true, release: true); } - /// download task convenience methods. When a download successfully - /// completes, the NSURL will point to a file that must be read or - /// copied during the invocation of the completion routine. The file - /// will be removed automatically. + /// downloadTaskWithRequest:completionHandler: NSURLSessionDownloadTask downloadTaskWithRequest_completionHandler_( NSURLRequest request, objc.ObjCBlock< @@ -77640,7 +77655,7 @@ class NSMutableCharacterSet extends objc.NSCharacterSet { retain: false, release: true); } - /// Returns a character set containing the characters allowed in a URL's user subcomponent. + /// URLUserAllowedCharacterSet static objc.NSCharacterSet getURLUserAllowedCharacterSet() { final _ret = _objc_msgSend_1unuoxw( _class_NSMutableCharacterSet, _sel_URLUserAllowedCharacterSet); @@ -77648,7 +77663,7 @@ class NSMutableCharacterSet extends objc.NSCharacterSet { retain: true, release: true); } - /// Returns a character set containing the characters allowed in a URL's password subcomponent. + /// URLPasswordAllowedCharacterSet static objc.NSCharacterSet getURLPasswordAllowedCharacterSet() { final _ret = _objc_msgSend_1unuoxw( _class_NSMutableCharacterSet, _sel_URLPasswordAllowedCharacterSet); @@ -77656,7 +77671,7 @@ class NSMutableCharacterSet extends objc.NSCharacterSet { retain: true, release: true); } - /// Returns a character set containing the characters allowed in a URL's host subcomponent. + /// URLHostAllowedCharacterSet static objc.NSCharacterSet getURLHostAllowedCharacterSet() { final _ret = _objc_msgSend_1unuoxw( _class_NSMutableCharacterSet, _sel_URLHostAllowedCharacterSet); @@ -77664,7 +77679,7 @@ class NSMutableCharacterSet extends objc.NSCharacterSet { retain: true, release: true); } - /// Returns a character set containing the characters allowed in a URL's path component. ';' is a legal path character, but it is recommended that it be percent-encoded for best compatibility with NSURL (-stringByAddingPercentEncodingWithAllowedCharacters: will percent-encode any ';' characters if you pass the URLPathAllowedCharacterSet). + /// URLPathAllowedCharacterSet static objc.NSCharacterSet getURLPathAllowedCharacterSet() { final _ret = _objc_msgSend_1unuoxw( _class_NSMutableCharacterSet, _sel_URLPathAllowedCharacterSet); @@ -77672,7 +77687,7 @@ class NSMutableCharacterSet extends objc.NSCharacterSet { retain: true, release: true); } - /// Returns a character set containing the characters allowed in a URL's query component. + /// URLQueryAllowedCharacterSet static objc.NSCharacterSet getURLQueryAllowedCharacterSet() { final _ret = _objc_msgSend_1unuoxw( _class_NSMutableCharacterSet, _sel_URLQueryAllowedCharacterSet); @@ -77680,7 +77695,7 @@ class NSMutableCharacterSet extends objc.NSCharacterSet { retain: true, release: true); } - /// Returns a character set containing the characters allowed in a URL's fragment component. + /// URLFragmentAllowedCharacterSet static objc.NSCharacterSet getURLFragmentAllowedCharacterSet() { final _ret = _objc_msgSend_1unuoxw( _class_NSMutableCharacterSet, _sel_URLFragmentAllowedCharacterSet); @@ -80237,6 +80252,222 @@ typedef NSErrorDomain = ffi.Pointer; typedef DartNSErrorDomain = objc.NSString; typedef NSErrorUserInfoKey = ffi.Pointer; typedef DartNSErrorUserInfoKey = objc.NSString; +typedef _DidFinish = ffi.Pointer; +typedef Dart_DidFinish = objc.ObjCBlock< + ffi.Void Function(ffi.Pointer, NSURLSession, + NSURLSessionDownloadTask, objc.NSURL)>; +typedef _DidFinishWithLock = ffi.Pointer; +typedef Dart_DidFinishWithLock = objc.ObjCBlock< + ffi.Void Function( + NSCondition, NSURLSession, NSURLSessionDownloadTask, objc.NSURL)>; +void + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_fnPtrTrampoline( + ffi.Pointer block, + ffi.Pointer arg0, + ffi.Pointer arg1, + ffi.Pointer arg2, + ffi.Pointer arg3) => + block.ref.target + .cast< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer arg0, + ffi.Pointer arg1, + ffi.Pointer arg2, + ffi.Pointer arg3)>>() + .asFunction< + void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>()(arg0, arg1, arg2, arg3); +ffi.Pointer + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_fnPtrCallable = + ffi.Pointer.fromFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>( + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_fnPtrTrampoline) + .cast(); +void + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_closureTrampoline( + ffi.Pointer block, + ffi.Pointer arg0, + ffi.Pointer arg1, + ffi.Pointer arg2, + ffi.Pointer arg3) => + (objc.getBlockClosure(block) as void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer))(arg0, arg1, arg2, arg3); +ffi.Pointer + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_closureCallable = + ffi.Pointer.fromFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>( + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_closureTrampoline) + .cast(); +void + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_listenerTrampoline( + ffi.Pointer block, + ffi.Pointer arg0, + ffi.Pointer arg1, + ffi.Pointer arg2, + ffi.Pointer arg3) { + (objc.getBlockClosure(block) as void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer))(arg0, arg1, arg2, arg3); + objc.objectRelease(block.cast()); +} + +ffi.NativeCallable< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)> + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_listenerCallable = + ffi.NativeCallable< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>.listener( + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_listenerTrampoline) + ..keepIsolateAlive = false; + +/// Construction methods for `objc.ObjCBlock`. +abstract final class ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL { + /// Returns a block that wraps the given raw block pointer. + static objc.ObjCBlock< + ffi.Void Function( + NSCondition, NSURLSession, NSURLSessionDownloadTask, objc.NSURL)> + castFromPointer(ffi.Pointer pointer, + {bool retain = false, bool release = false}) => + objc.ObjCBlock< + ffi.Void Function( + NSCondition, + NSURLSession, + NSURLSessionDownloadTask, + objc.NSURL)>(pointer, retain: retain, release: release); + + /// Creates a block from a C function pointer. + /// + /// This block must be invoked by native code running on the same thread as + /// the isolate that registered it. Invoking the block on the wrong thread + /// will result in a crash. + static objc.ObjCBlock + fromFunctionPointer(ffi.Pointer arg0, ffi.Pointer arg1, ffi.Pointer arg2, ffi.Pointer arg3)>> ptr) => + objc.ObjCBlock< + ffi.Void Function(NSCondition, NSURLSession, + NSURLSessionDownloadTask, objc.NSURL)>( + objc.newPointerBlock( + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_fnPtrCallable, + ptr.cast()), + retain: false, + release: true); + + /// Creates a block from a Dart function. + /// + /// This block must be invoked by native code running on the same thread as + /// the isolate that registered it. Invoking the block on the wrong thread + /// will result in a crash. + static objc.ObjCBlock fromFunction(void Function(NSCondition, NSURLSession, NSURLSessionDownloadTask, objc.NSURL) fn) => + objc.ObjCBlock( + objc.newClosureBlock( + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_closureCallable, + (ffi.Pointer arg0, + ffi.Pointer arg1, + ffi.Pointer arg2, + ffi.Pointer arg3) => + fn( + NSCondition.castFromPointer(arg0, retain: true, release: true), + NSURLSession.castFromPointer(arg1, retain: true, release: true), + NSURLSessionDownloadTask.castFromPointer(arg2, retain: true, release: true), + objc.NSURL.castFromPointer(arg3, retain: true, release: true))), + retain: false, + release: true); + + /// Creates a listener block from a Dart function. + /// + /// This is based on FFI's NativeCallable.listener, and has the same + /// capabilities and limitations. This block can be invoked from any thread, + /// but only supports void functions, and is not run synchronously. See + /// NativeCallable.listener for more details. + /// + /// Note that unlike the default behavior of NativeCallable.listener, listener + /// blocks do not keep the isolate alive. + static objc.ObjCBlock< + ffi.Void Function(NSCondition, NSURLSession, NSURLSessionDownloadTask, + objc.NSURL)> listener( + void Function( + NSCondition, NSURLSession, NSURLSessionDownloadTask, objc.NSURL) + fn) { + final raw = objc.newClosureBlock( + _ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_listenerCallable + .nativeFunction + .cast(), + (ffi.Pointer arg0, + ffi.Pointer arg1, + ffi.Pointer arg2, + ffi.Pointer arg3) => + fn( + NSCondition.castFromPointer(arg0, retain: false, release: true), + NSURLSession.castFromPointer(arg1, + retain: false, release: true), + NSURLSessionDownloadTask.castFromPointer(arg2, + retain: false, release: true), + objc.NSURL + .castFromPointer(arg3, retain: false, release: true))); + final wrapper = _wrapListenerBlock_19b8ge5(raw); + objc.objectRelease(raw.cast()); + return objc.ObjCBlock< + ffi.Void Function(NSCondition, NSURLSession, NSURLSessionDownloadTask, + objc.NSURL)>(wrapper, retain: false, release: true); + } +} + +/// Call operator for `objc.ObjCBlock`. +extension ObjCBlock_ffiVoid_NSCondition_NSURLSession_NSURLSessionDownloadTask_NSURL_CallExtension + on objc.ObjCBlock< + ffi.Void Function( + NSCondition, NSURLSession, NSURLSessionDownloadTask, objc.NSURL)> { + void call(NSCondition arg0, NSURLSession arg1, NSURLSessionDownloadTask arg2, + objc.NSURL arg3) => + ref.pointer.ref.invoke + .cast< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer block, + ffi.Pointer arg0, + ffi.Pointer arg1, + ffi.Pointer arg2, + ffi.Pointer arg3)>>() + .asFunction< + void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>()( + ref.pointer, + arg0.ref.pointer, + arg1.ref.pointer, + arg2.ref.pointer, + arg3.ref.pointer); +} const int noErr = 0;