[vm/ffi] `Pointer.asTypedList` `finalizer`

TEST=tests/ffi/external_typed_data_finalizer_test.dart

Closes: https://github.com/dart-lang/sdk/issues/50507
CoreLibraryReviewExempt: https://github.com/dart-lang/sdk/issues/52261
Change-Id: I1a82dcca15961b28c0de64637970fe38a39286e5
Cq-Include-Trybots: luci.dart.try:vm-asan-linux-release-x64-try,vm-aot-asan-linux-release-x64-try,vm-ffi-android-debug-arm-try,vm-ffi-qemu-linux-release-arm-try,vm-win-debug-x64-try,vm-win-debug-x64c-try,vm-aot-win-debug-x64c-try,vm-mac-debug-arm64-try,vm-mac-debug-x64-try,vm-aot-mac-release-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/301001
Commit-Queue: Daco Harkes <dacoharkes@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
diff --git a/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc b/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc
index 584f7ed..afa389d 100644
--- a/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc
+++ b/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc
@@ -8,6 +8,7 @@
 #include <stdlib.h>
 #include <sys/types.h>
 #include <csignal>
+#include <cstdlib>
 
 #include "platform/globals.h"
 #include "platform/memory_sanitizer.h"
@@ -1316,4 +1317,40 @@
   return Dart_IsNull(object);
 }
 
+namespace {
+struct RefCountedResource {
+  void* resource;
+  intptr_t refcount;
+};
+}  // namespace
+
+// We only ever have one ref counted resource in our test, use global lock.
+std::mutex ref_counted_resource_mutex;
+
+DART_EXPORT RefCountedResource* AllocateRefcountedResource() {
+  auto peer =
+      static_cast<RefCountedResource*>(malloc(sizeof(RefCountedResource)));
+  auto resource = malloc(128);
+  peer->resource = resource;
+  peer->refcount = 0;  // We're not going to count the reference here.
+  return peer;
+}
+
+DART_EXPORT void IncreaseRefcount(RefCountedResource* peer) {
+  ref_counted_resource_mutex.lock();
+  peer->refcount++;
+  ref_counted_resource_mutex.unlock();
+}
+
+// And delete if zero.
+DART_EXPORT void DecreaseRefcount(RefCountedResource* peer) {
+  ref_counted_resource_mutex.lock();
+  peer->refcount--;
+  if (peer->refcount <= 0) {
+    free(peer->resource);
+    free(peer);
+  }
+  ref_counted_resource_mutex.unlock();
+}
+
 }  // namespace dart
diff --git a/runtime/lib/ffi.cc b/runtime/lib/ffi.cc
index 4893b7c..68eeeae 100644
--- a/runtime/lib/ffi.cc
+++ b/runtime/lib/ffi.cc
@@ -189,4 +189,27 @@
   }
 };
 
+namespace {
+struct AsTypedListFinalizerData {
+  void (*callback)(void*);
+  void* token;
+};
+}  // namespace
+
+DEFINE_FFI_NATIVE_ENTRY(Pointer_asTypedListFinalizerAllocateData, void*, ()) {
+  return malloc(sizeof(AsTypedListFinalizerData));
+};
+
+void AsTypedListFinalizerCallback(void* peer) {
+  const auto* data = reinterpret_cast<AsTypedListFinalizerData*>(peer);
+  data->callback(data->token);
+  free(peer);
+}
+
+DEFINE_FFI_NATIVE_ENTRY(Pointer_asTypedListFinalizerCallbackPointer,
+                        void*,
+                        ()) {
+  return reinterpret_cast<void*>(&AsTypedListFinalizerCallback);
+};
+
 }  // namespace dart
diff --git a/runtime/tools/ffi/sdk_lib_ffi_generator.dart b/runtime/tools/ffi/sdk_lib_ffi_generator.dart
index 27eda6f..e8919df 100644
--- a/runtime/tools/ffi/sdk_lib_ffi_generator.dart
+++ b/runtime/tools/ffi/sdk_lib_ffi_generator.dart
@@ -39,30 +39,51 @@
   final args = argParser().parse(arguments);
   Uri path = Uri.parse(args['path']);
 
-  generate(path, "ffi.g.dart", generatePublicExtension);
-  generate(path, "ffi_patch.g.dart", generatePatchExtension);
+  update(Uri.file('sdk/lib/ffi/ffi.dart'), generatePublicExtension);
+  update(Uri.file('sdk/lib/_internal/vm/lib/ffi_patch.dart'),
+      generatePatchExtension);
 }
 
-void generate(Uri path, String fileName,
-    Function(StringBuffer, Config, String) generator) {
+void update(Uri fileName, Function(StringBuffer, Config, String) generator) {
+  final file = File.fromUri(fileName);
+  if (!file.existsSync()) {
+    print('$fileName does not exist, run from the root of the SDK.');
+    return;
+  }
+
+  final fileContents = file.readAsStringSync();
+  final split1 = fileContents.split(header);
+  if (split1.length != 2) {
+    print('$fileName has unexpected contents.');
+    print(split1.length);
+    return;
+  }
+  final split2 = split1[1].split(footer);
+  if (split2.length != 2) {
+    print('$fileName has unexpected contents 2.');
+    print(split2.length);
+    return;
+  }
+
   final buffer = StringBuffer();
-  generateHeader(buffer);
+  buffer.write(split1[0]);
+  buffer.write(header);
   configuration.forEach((Config c) => generator(buffer, c, "Pointer"));
   configuration.forEach((Config c) => generator(buffer, c, "Array"));
-  generateFooter(buffer);
+  buffer.write(footer);
+  buffer.write(split2[1]);
 
-  final fullPath = path.resolve(fileName).path;
-  File(fullPath).writeAsStringSync(buffer.toString());
-  final fmtResult = Process.runSync(dartPath().path, ["format", fullPath]);
+  file.writeAsStringSync(buffer.toString());
+  final fmtResult =
+      Process.runSync(dartPath().path, ["format", fileName.toFilePath()]);
   if (fmtResult.exitCode != 0) {
     throw Exception(
         "Formatting failed:\n${fmtResult.stdout}\n${fmtResult.stderr}\n");
   }
-  print("Generated $fullPath.");
+  print("Updated $fileName.");
 }
 
-void generateHeader(StringBuffer buffer) {
-  const header = """
+const header = """
 //
 // The following code is generated, do not edit by hand.
 //
@@ -71,6 +92,7 @@
 
 """;
 
+void generateHeader(StringBuffer buffer) {
   buffer.write(header);
 }
 
@@ -173,7 +195,15 @@
   ///
   /// The user has to ensure the memory range is accessible while using the
   /// returned list.
-$alignment  external $typedListType asTypedList(int length);
+  ///
+  /// If provided, [finalizer] will be run on the pointer once the typed list
+  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
+  /// the this pointer itself will be passed.
+$alignment  external $typedListType asTypedList(
+    int length, {
+    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
+    @Since('3.1') Pointer<Void>? token,
+  });
 """;
 
   if (container == "Pointer") {
@@ -228,12 +258,20 @@
       ? ""
       : """
   @patch
-  $typedListType asTypedList(int length) {
+  $typedListType asTypedList(
+    int length, {
+     Pointer<NativeFinalizerFunction>? finalizer,
+     Pointer<Void>? token,
+  }) {
     ArgumentError.checkNotNull(this, "Pointer<$nativeType>");
     ArgumentError.checkNotNull(length, "length");
     _checkExternalTypedDataLength(length, $elementSize);
     _checkPointerAlignment(address, $elementSize);
-    return _asExternalTypedData$nativeType(this, length);
+    final result = _asExternalTypedData$nativeType(this, length);
+    if (finalizer != null) {
+      _attachAsTypedListFinalizer(finalizer, result, token ?? this, $sizeTimes length);
+    }
+    return result;
   }
 """;
 
@@ -279,13 +317,13 @@
   }
 }
 
-void generateFooter(StringBuffer buffer) {
-  final footer = """
+final footer = """
 //
 // End of generated code.
 //
 """;
 
+void generateFooter(StringBuffer buffer) {
   buffer.write(footer);
 }
 
diff --git a/runtime/vm/bootstrap_natives.cc b/runtime/vm/bootstrap_natives.cc
index 63d1e16..c63d4f1 100644
--- a/runtime/vm/bootstrap_natives.cc
+++ b/runtime/vm/bootstrap_natives.cc
@@ -127,6 +127,7 @@
   ASSERT(!library.IsNull());
   library.set_native_entry_resolver(resolver);
   library.set_native_entry_symbol_resolver(symbol_resolver);
+  library.set_ffi_native_resolver(ffi_native_resolver);
 
   library = Library::InternalLibrary();
   ASSERT(!library.IsNull());
diff --git a/runtime/vm/bootstrap_natives.h b/runtime/vm/bootstrap_natives.h
index 433e7d4..cf772df 100644
--- a/runtime/vm/bootstrap_natives.h
+++ b/runtime/vm/bootstrap_natives.h
@@ -466,7 +466,9 @@
   V(VariableMirror_type, 2)
 
 #define BOOTSTRAP_FFI_NATIVE_LIST(V)                                           \
-  V(FinalizerEntry_SetExternalSize, void, (Dart_Handle, intptr_t))
+  V(FinalizerEntry_SetExternalSize, void, (Dart_Handle, intptr_t))             \
+  V(Pointer_asTypedListFinalizerAllocateData, void*, ())                       \
+  V(Pointer_asTypedListFinalizerCallbackPointer, void*, ())
 
 class BootstrapNatives : public AllStatic {
  public:
@@ -474,7 +476,7 @@
                                     int argument_count,
                                     bool* auto_setup_scope);
 
-  // For use with @FfiNative.
+  // For use with @Native.
   static void* LookupFfiNative(const char* name, uintptr_t argument_count);
 
   static const uint8_t* Symbol(Dart_NativeFunction nf);
diff --git a/runtime/vm/kernel_loader.cc b/runtime/vm/kernel_loader.cc
index 6ea6152..4a2d1df 100644
--- a/runtime/vm/kernel_loader.cc
+++ b/runtime/vm/kernel_loader.cc
@@ -1207,7 +1207,8 @@
     if (!Api::IsFfiEnabled() &&
         target_library.url() == Symbols::DartFfi().ptr() &&
         library->url() != Symbols::DartCore().ptr() &&
-        library->url() != Symbols::DartInternal().ptr()) {
+        library->url() != Symbols::DartInternal().ptr() &&
+        library->url() != Symbols::DartFfi().ptr()) {
       H.ReportError(
           "import of dart:ffi is not supported in the current Dart runtime");
     }
diff --git a/sdk/lib/_internal/vm/lib/ffi_native_finalizer_patch.dart b/sdk/lib/_internal/vm/lib/ffi_native_finalizer_patch.dart
index d2c387c..e9483d5 100644
--- a/sdk/lib/_internal/vm/lib/ffi_native_finalizer_patch.dart
+++ b/sdk/lib/_internal/vm/lib/ffi_native_finalizer_patch.dart
@@ -5,6 +5,7 @@
 // All imports must be in all FFI patch files to not depend on the order
 // the patches are applied.
 import 'dart:_internal';
+import 'dart:ffi';
 import 'dart:isolate';
 import 'dart:typed_data';
 
@@ -38,7 +39,7 @@
   }
 
   void attach(
-    Finalizable value,
+    Object value,
     Pointer<Void> token, {
     Object? detach,
     int? externalSize,
@@ -102,3 +103,43 @@
     finalizer._removeEntries();
   }
 }
+
+@Native<Pointer<NativeFinalizerFunction> Function()>(
+    symbol: 'Pointer_asTypedListFinalizerCallbackPointer')
+external Pointer<NativeFinalizerFunction>
+    _asTypedListFinalizerCallbackPointer();
+
+final Pointer<NativeFinalizerFunction> _asTypedListFinalizerCallback =
+    _asTypedListFinalizerCallbackPointer();
+
+final _asTypedListFinalizer = _NativeFinalizer(_asTypedListFinalizerCallback);
+
+final class _AsTypedListFinalizerData extends Struct {
+  external Pointer<NativeFinalizerFunction> callback;
+  external Pointer<Void> token;
+}
+
+@patch
+void _attachAsTypedListFinalizer(
+  Pointer<NativeFinalizerFunction> callback,
+  Object typedList,
+  Pointer pointer,
+  int? externalSize,
+) {
+  final data = _allocateData();
+  data.ref.callback = callback;
+  data.ref.token = pointer.cast();
+  _asTypedListFinalizer.attach(
+    typedList,
+    data.cast(),
+    externalSize: externalSize,
+  );
+}
+
+// Ensure we use the `malloc` that corresponds to the `free` used inside
+// `_asTypedListFinalizerCallback` in the VM.
+@Native<Pointer<_AsTypedListFinalizerData> Function()>(
+  symbol: 'Pointer_asTypedListFinalizerAllocateData',
+  isLeaf: true,
+)
+external Pointer<_AsTypedListFinalizerData> _allocateData();
diff --git a/sdk/lib/_internal/vm/lib/ffi_patch.dart b/sdk/lib/_internal/vm/lib/ffi_patch.dart
index f06582e..5f362b1 100644
--- a/sdk/lib/_internal/vm/lib/ffi_patch.dart
+++ b/sdk/lib/_internal/vm/lib/ffi_patch.dart
@@ -451,12 +451,20 @@
   Pointer<Int8> elementAt(int index) => Pointer.fromAddress(address + index);
 
   @patch
-  Int8List asTypedList(int length) {
+  Int8List asTypedList(
+    int length, {
+    Pointer<NativeFinalizerFunction>? finalizer,
+    Pointer<Void>? token,
+  }) {
     ArgumentError.checkNotNull(this, "Pointer<Int8>");
     ArgumentError.checkNotNull(length, "length");
     _checkExternalTypedDataLength(length, 1);
     _checkPointerAlignment(address, 1);
-    return _asExternalTypedDataInt8(this, length);
+    final result = _asExternalTypedDataInt8(this, length);
+    if (finalizer != null) {
+      _attachAsTypedListFinalizer(finalizer, result, token ?? this, length);
+    }
+    return result;
   }
 }
 
@@ -478,12 +486,20 @@
       Pointer.fromAddress(address + 2 * index);
 
   @patch
-  Int16List asTypedList(int length) {
+  Int16List asTypedList(
+    int length, {
+    Pointer<NativeFinalizerFunction>? finalizer,
+    Pointer<Void>? token,
+  }) {
     ArgumentError.checkNotNull(this, "Pointer<Int16>");
     ArgumentError.checkNotNull(length, "length");
     _checkExternalTypedDataLength(length, 2);
     _checkPointerAlignment(address, 2);
-    return _asExternalTypedDataInt16(this, length);
+    final result = _asExternalTypedDataInt16(this, length);
+    if (finalizer != null) {
+      _attachAsTypedListFinalizer(finalizer, result, token ?? this, 2 * length);
+    }
+    return result;
   }
 }
 
@@ -505,12 +521,20 @@
       Pointer.fromAddress(address + 4 * index);
 
   @patch
-  Int32List asTypedList(int length) {
+  Int32List asTypedList(
+    int length, {
+    Pointer<NativeFinalizerFunction>? finalizer,
+    Pointer<Void>? token,
+  }) {
     ArgumentError.checkNotNull(this, "Pointer<Int32>");
     ArgumentError.checkNotNull(length, "length");
     _checkExternalTypedDataLength(length, 4);
     _checkPointerAlignment(address, 4);
-    return _asExternalTypedDataInt32(this, length);
+    final result = _asExternalTypedDataInt32(this, length);
+    if (finalizer != null) {
+      _attachAsTypedListFinalizer(finalizer, result, token ?? this, 4 * length);
+    }
+    return result;
   }
 }
 
@@ -532,12 +556,20 @@
       Pointer.fromAddress(address + 8 * index);
 
   @patch
-  Int64List asTypedList(int length) {
+  Int64List asTypedList(
+    int length, {
+    Pointer<NativeFinalizerFunction>? finalizer,
+    Pointer<Void>? token,
+  }) {
     ArgumentError.checkNotNull(this, "Pointer<Int64>");
     ArgumentError.checkNotNull(length, "length");
     _checkExternalTypedDataLength(length, 8);
     _checkPointerAlignment(address, 8);
-    return _asExternalTypedDataInt64(this, length);
+    final result = _asExternalTypedDataInt64(this, length);
+    if (finalizer != null) {
+      _attachAsTypedListFinalizer(finalizer, result, token ?? this, 8 * length);
+    }
+    return result;
   }
 }
 
@@ -558,12 +590,20 @@
   Pointer<Uint8> elementAt(int index) => Pointer.fromAddress(address + index);
 
   @patch
-  Uint8List asTypedList(int length) {
+  Uint8List asTypedList(
+    int length, {
+    Pointer<NativeFinalizerFunction>? finalizer,
+    Pointer<Void>? token,
+  }) {
     ArgumentError.checkNotNull(this, "Pointer<Uint8>");
     ArgumentError.checkNotNull(length, "length");
     _checkExternalTypedDataLength(length, 1);
     _checkPointerAlignment(address, 1);
-    return _asExternalTypedDataUint8(this, length);
+    final result = _asExternalTypedDataUint8(this, length);
+    if (finalizer != null) {
+      _attachAsTypedListFinalizer(finalizer, result, token ?? this, length);
+    }
+    return result;
   }
 }
 
@@ -585,12 +625,20 @@
       Pointer.fromAddress(address + 2 * index);
 
   @patch
-  Uint16List asTypedList(int length) {
+  Uint16List asTypedList(
+    int length, {
+    Pointer<NativeFinalizerFunction>? finalizer,
+    Pointer<Void>? token,
+  }) {
     ArgumentError.checkNotNull(this, "Pointer<Uint16>");
     ArgumentError.checkNotNull(length, "length");
     _checkExternalTypedDataLength(length, 2);
     _checkPointerAlignment(address, 2);
-    return _asExternalTypedDataUint16(this, length);
+    final result = _asExternalTypedDataUint16(this, length);
+    if (finalizer != null) {
+      _attachAsTypedListFinalizer(finalizer, result, token ?? this, 2 * length);
+    }
+    return result;
   }
 }
 
@@ -612,12 +660,20 @@
       Pointer.fromAddress(address + 4 * index);
 
   @patch
-  Uint32List asTypedList(int length) {
+  Uint32List asTypedList(
+    int length, {
+    Pointer<NativeFinalizerFunction>? finalizer,
+    Pointer<Void>? token,
+  }) {
     ArgumentError.checkNotNull(this, "Pointer<Uint32>");
     ArgumentError.checkNotNull(length, "length");
     _checkExternalTypedDataLength(length, 4);
     _checkPointerAlignment(address, 4);
-    return _asExternalTypedDataUint32(this, length);
+    final result = _asExternalTypedDataUint32(this, length);
+    if (finalizer != null) {
+      _attachAsTypedListFinalizer(finalizer, result, token ?? this, 4 * length);
+    }
+    return result;
   }
 }
 
@@ -639,12 +695,20 @@
       Pointer.fromAddress(address + 8 * index);
 
   @patch
-  Uint64List asTypedList(int length) {
+  Uint64List asTypedList(
+    int length, {
+    Pointer<NativeFinalizerFunction>? finalizer,
+    Pointer<Void>? token,
+  }) {
     ArgumentError.checkNotNull(this, "Pointer<Uint64>");
     ArgumentError.checkNotNull(length, "length");
     _checkExternalTypedDataLength(length, 8);
     _checkPointerAlignment(address, 8);
-    return _asExternalTypedDataUint64(this, length);
+    final result = _asExternalTypedDataUint64(this, length);
+    if (finalizer != null) {
+      _attachAsTypedListFinalizer(finalizer, result, token ?? this, 8 * length);
+    }
+    return result;
   }
 }
 
@@ -666,12 +730,20 @@
       Pointer.fromAddress(address + 4 * index);
 
   @patch
-  Float32List asTypedList(int length) {
+  Float32List asTypedList(
+    int length, {
+    Pointer<NativeFinalizerFunction>? finalizer,
+    Pointer<Void>? token,
+  }) {
     ArgumentError.checkNotNull(this, "Pointer<Float>");
     ArgumentError.checkNotNull(length, "length");
     _checkExternalTypedDataLength(length, 4);
     _checkPointerAlignment(address, 4);
-    return _asExternalTypedDataFloat(this, length);
+    final result = _asExternalTypedDataFloat(this, length);
+    if (finalizer != null) {
+      _attachAsTypedListFinalizer(finalizer, result, token ?? this, 4 * length);
+    }
+    return result;
   }
 }
 
@@ -693,12 +765,20 @@
       Pointer.fromAddress(address + 8 * index);
 
   @patch
-  Float64List asTypedList(int length) {
+  Float64List asTypedList(
+    int length, {
+    Pointer<NativeFinalizerFunction>? finalizer,
+    Pointer<Void>? token,
+  }) {
     ArgumentError.checkNotNull(this, "Pointer<Double>");
     ArgumentError.checkNotNull(length, "length");
     _checkExternalTypedDataLength(length, 8);
     _checkPointerAlignment(address, 8);
-    return _asExternalTypedDataDouble(this, length);
+    final result = _asExternalTypedDataDouble(this, length);
+    if (finalizer != null) {
+      _attachAsTypedListFinalizer(finalizer, result, token ?? this, 8 * length);
+    }
+    return result;
   }
 }
 
diff --git a/sdk/lib/_internal/vm/lib/internal_patch.dart b/sdk/lib/_internal/vm/lib/internal_patch.dart
index 301287b..7b03d9f 100644
--- a/sdk/lib/_internal/vm/lib/internal_patch.dart
+++ b/sdk/lib/_internal/vm/lib/internal_patch.dart
@@ -9,7 +9,7 @@
 
 import "dart:async" show Timer;
 import "dart:core" hide Symbol;
-import "dart:ffi" show Pointer, Struct, Union, IntPtr, Handle, Void, FfiNative;
+import "dart:ffi" show Pointer, Struct, Union, IntPtr, Handle, Void, Native;
 import "dart:isolate" show SendPort;
 import "dart:typed_data" show Int32List, Uint8List;
 
@@ -425,7 +425,8 @@
   external int get externalSize;
 
   /// Update the external size.
-  @FfiNative<Void Function(Handle, IntPtr)>('FinalizerEntry_SetExternalSize')
+  @Native<Void Function(Handle, IntPtr)>(
+      symbol: 'FinalizerEntry_SetExternalSize')
   external void setExternalSize(int externalSize);
 }
 
diff --git a/sdk/lib/ffi/ffi.dart b/sdk/lib/ffi/ffi.dart
index 2ddc8ca..5041e99 100644
--- a/sdk/lib/ffi/ffi.dart
+++ b/sdk/lib/ffi/ffi.dart
@@ -193,7 +193,15 @@
   ///
   /// The user has to ensure the memory range is accessible while using the
   /// returned list.
-  external Int8List asTypedList(int length);
+  ///
+  /// If provided, [finalizer] will be run on the pointer once the typed list
+  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
+  /// the this pointer itself will be passed.
+  external Int8List asTypedList(
+    int length, {
+    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
+    @Since('3.1') Pointer<Void>? token,
+  });
 }
 
 /// Extension on [Pointer] specialized for the type argument [Int16].
@@ -235,8 +243,16 @@
   /// The user has to ensure the memory range is accessible while using the
   /// returned list.
   ///
+  /// If provided, [finalizer] will be run on the pointer once the typed list
+  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
+  /// the this pointer itself will be passed.
+  ///
   /// The [address] must be 2-byte aligned.
-  external Int16List asTypedList(int length);
+  external Int16List asTypedList(
+    int length, {
+    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
+    @Since('3.1') Pointer<Void>? token,
+  });
 }
 
 /// Extension on [Pointer] specialized for the type argument [Int32].
@@ -278,8 +294,16 @@
   /// The user has to ensure the memory range is accessible while using the
   /// returned list.
   ///
+  /// If provided, [finalizer] will be run on the pointer once the typed list
+  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
+  /// the this pointer itself will be passed.
+  ///
   /// The [address] must be 4-byte aligned.
-  external Int32List asTypedList(int length);
+  external Int32List asTypedList(
+    int length, {
+    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
+    @Since('3.1') Pointer<Void>? token,
+  });
 }
 
 /// Extension on [Pointer] specialized for the type argument [Int64].
@@ -312,8 +336,16 @@
   /// The user has to ensure the memory range is accessible while using the
   /// returned list.
   ///
+  /// If provided, [finalizer] will be run on the pointer once the typed list
+  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
+  /// the this pointer itself will be passed.
+  ///
   /// The [address] must be 8-byte aligned.
-  external Int64List asTypedList(int length);
+  external Int64List asTypedList(
+    int length, {
+    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
+    @Since('3.1') Pointer<Void>? token,
+  });
 }
 
 /// Extension on [Pointer] specialized for the type argument [Uint8].
@@ -348,7 +380,15 @@
   ///
   /// The user has to ensure the memory range is accessible while using the
   /// returned list.
-  external Uint8List asTypedList(int length);
+  ///
+  /// If provided, [finalizer] will be run on the pointer once the typed list
+  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
+  /// the this pointer itself will be passed.
+  external Uint8List asTypedList(
+    int length, {
+    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
+    @Since('3.1') Pointer<Void>? token,
+  });
 }
 
 /// Extension on [Pointer] specialized for the type argument [Uint16].
@@ -390,8 +430,16 @@
   /// The user has to ensure the memory range is accessible while using the
   /// returned list.
   ///
+  /// If provided, [finalizer] will be run on the pointer once the typed list
+  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
+  /// the this pointer itself will be passed.
+  ///
   /// The [address] must be 2-byte aligned.
-  external Uint16List asTypedList(int length);
+  external Uint16List asTypedList(
+    int length, {
+    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
+    @Since('3.1') Pointer<Void>? token,
+  });
 }
 
 /// Extension on [Pointer] specialized for the type argument [Uint32].
@@ -433,8 +481,16 @@
   /// The user has to ensure the memory range is accessible while using the
   /// returned list.
   ///
+  /// If provided, [finalizer] will be run on the pointer once the typed list
+  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
+  /// the this pointer itself will be passed.
+  ///
   /// The [address] must be 4-byte aligned.
-  external Uint32List asTypedList(int length);
+  external Uint32List asTypedList(
+    int length, {
+    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
+    @Since('3.1') Pointer<Void>? token,
+  });
 }
 
 /// Extension on [Pointer] specialized for the type argument [Uint64].
@@ -467,8 +523,16 @@
   /// The user has to ensure the memory range is accessible while using the
   /// returned list.
   ///
+  /// If provided, [finalizer] will be run on the pointer once the typed list
+  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
+  /// the this pointer itself will be passed.
+  ///
   /// The [address] must be 8-byte aligned.
-  external Uint64List asTypedList(int length);
+  external Uint64List asTypedList(
+    int length, {
+    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
+    @Since('3.1') Pointer<Void>? token,
+  });
 }
 
 /// Extension on [Pointer] specialized for the type argument [Float].
@@ -510,8 +574,16 @@
   /// The user has to ensure the memory range is accessible while using the
   /// returned list.
   ///
+  /// If provided, [finalizer] will be run on the pointer once the typed list
+  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
+  /// the this pointer itself will be passed.
+  ///
   /// The [address] must be 4-byte aligned.
-  external Float32List asTypedList(int length);
+  external Float32List asTypedList(
+    int length, {
+    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
+    @Since('3.1') Pointer<Void>? token,
+  });
 }
 
 /// Extension on [Pointer] specialized for the type argument [Double].
@@ -544,8 +616,16 @@
   /// The user has to ensure the memory range is accessible while using the
   /// returned list.
   ///
+  /// If provided, [finalizer] will be run on the pointer once the typed list
+  /// is GCed. If provided, [token] will be passed to [finalizer], otherwise
+  /// the this pointer itself will be passed.
+  ///
   /// The [address] must be 8-byte aligned.
-  external Float64List asTypedList(int length);
+  external Float64List asTypedList(
+    int length, {
+    @Since('3.1') Pointer<NativeFinalizerFunction>? finalizer,
+    @Since('3.1') Pointer<Void>? token,
+  });
 }
 
 /// Extension on [Pointer] specialized for the type argument [Bool].
diff --git a/sdk/lib/ffi/native_finalizer.dart b/sdk/lib/ffi/native_finalizer.dart
index bba05b1..d1bd464 100644
--- a/sdk/lib/ffi/native_finalizer.dart
+++ b/sdk/lib/ffi/native_finalizer.dart
@@ -387,3 +387,11 @@
   /// object become inaccessible.
   void detach(Object detach);
 }
+
+// To make dart2wasm compile without patch file.
+external void _attachAsTypedListFinalizer(
+  Pointer<NativeFinalizerFunction> callback,
+  Object typedList,
+  Pointer pointer,
+  int? externalSize,
+);
diff --git a/tests/ffi/external_typed_data_finalizer_test.dart b/tests/ffi/external_typed_data_finalizer_test.dart
new file mode 100644
index 0000000..1575fc9
--- /dev/null
+++ b/tests/ffi/external_typed_data_finalizer_test.dart
@@ -0,0 +1,62 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// SharedObjects=ffi_test_functions
+
+import 'dart:ffi';
+
+import 'dylib_utils.dart';
+
+main() {
+  // Force dlopen so @Native lookups in DynamicLibrary.process() succeed.
+  dlopenGlobalPlatformSpecific('ffi_test_functions');
+
+  testAsTypedList();
+  testRefcounted();
+}
+
+void testAsTypedList() {
+  const length = 10;
+  final ptr = calloc(length, sizeOf<Int16>()).cast<Int16>();
+  final typedList = ptr.asTypedList(length, finalizer: freePointer);
+  print(typedList);
+}
+
+@Native<Pointer<Void> Function(IntPtr num, IntPtr size)>(isLeaf: true)
+external Pointer<Void> calloc(int num, int size);
+
+final freePointer = DynamicLibrary.process()
+    .lookup<NativeFunction<Void Function(Pointer<Void>)>>('free');
+
+void testRefcounted() {
+  final peer = allocateRefcountedResource();
+  final resource = peer.ref.resource;
+  final typedList1 = resource.asTypedList(128,
+      finalizer: decreaseRefcountPointer.cast(), token: peer.cast());
+  increaseRefcount(peer);
+  print(typedList1);
+  final typedList2 = resource.asTypedList(128,
+      finalizer: decreaseRefcountPointer.cast(), token: peer.cast());
+  increaseRefcount(peer);
+  print(typedList2);
+}
+
+@Native<Pointer<RefCountedResource> Function()>(
+    symbol: 'AllocateRefcountedResource', isLeaf: true)
+external Pointer<RefCountedResource> allocateRefcountedResource();
+
+@Native<Void Function(Pointer<RefCountedResource>)>(
+    symbol: 'IncreaseRefcount', isLeaf: true)
+external void increaseRefcount(Pointer<RefCountedResource> peer);
+
+final decreaseRefcountPointer = DynamicLibrary.process()
+    .lookup<NativeFunction<Void Function(Pointer<RefCountedResource>)>>(
+        'DecreaseRefcount');
+
+final class RefCountedResource extends Struct {
+  external Pointer<Int8> resource;
+
+  @IntPtr()
+  external int refcount;
+}
diff --git a/tests/lib/mirrors/invocation_fuzz_test.dart b/tests/lib/mirrors/invocation_fuzz_test.dart
index c51e1fd..c060639 100644
--- a/tests/lib/mirrors/invocation_fuzz_test.dart
+++ b/tests/lib/mirrors/invocation_fuzz_test.dart
@@ -42,6 +42,9 @@
   // Don't try to invoke FFI Natives on simulator.
   // TODO(http://dartbug.com/48365): Support FFI in simulators.
   'dart._internal.FinalizerEntry.setExternalSize',
+
+  // Don't instantiate structs with bogus memory.
+  'dart.ffi._AsTypedListFinalizerData',
 ];
 
 bool isDenylisted(Symbol qualifiedSymbol) {
diff --git a/tests/lib_2/mirrors/invocation_fuzz_test.dart b/tests/lib_2/mirrors/invocation_fuzz_test.dart
index 038ad39..7b1ae88 100644
--- a/tests/lib_2/mirrors/invocation_fuzz_test.dart
+++ b/tests/lib_2/mirrors/invocation_fuzz_test.dart
@@ -44,6 +44,9 @@
   // Don't try to invoke FFI Natives on simulator.
   // TODO(http://dartbug.com/48365): Support FFI in simulators.
   'dart._internal.FinalizerEntry.setExternalSize',
+
+  // Don't instantiate structs with bogus memory.
+  'dart.ffi._AsTypedListFinalizerData',
 ];
 
 bool isDenylisted(Symbol qualifiedSymbol) {