Expose pointer to free from allocators (#203)

diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index 03731f5..64e4340 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -47,7 +47,7 @@
       matrix:
         # Add macos-latest and/or windows-latest if relevant for this package.
         os: [ubuntu-latest]
-        sdk: [2.17.0, dev]
+        sdk: [3.0.0, dev]
     steps:
       - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9
       - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ecb903..1632b9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 2.1.0
+
+- Require Dart 3.0.0 or greater.
+- Expose native equivalent to `free` (`nativeFree`) from `malloc` and
+  `calloc` allocators.
+
 ## 2.0.2
 
 - Fixed a typo in a doc comment.
diff --git a/lib/src/allocation.dart b/lib/src/allocation.dart
index 8b58884..4202145 100644
--- a/lib/src/allocation.dart
+++ b/lib/src/allocation.dart
@@ -22,19 +22,21 @@
 
 typedef PosixFreeNative = Void Function(Pointer);
 typedef PosixFree = void Function(Pointer);
-final PosixFree posixFree =
-    stdlib.lookupFunction<PosixFreeNative, PosixFree>('free');
+final Pointer<NativeFunction<PosixFreeNative>> posixFreePointer =
+    stdlib.lookup('free');
+final PosixFree posixFree = posixFreePointer.asFunction();
 
-typedef WinCoTaskMemAllocNative = Pointer Function(Size cb);
-typedef WinCoTaskMemAlloc = Pointer Function(int cb);
+typedef WinCoTaskMemAllocNative = Pointer Function(Size);
+typedef WinCoTaskMemAlloc = Pointer Function(int);
 final WinCoTaskMemAlloc winCoTaskMemAlloc =
     stdlib.lookupFunction<WinCoTaskMemAllocNative, WinCoTaskMemAlloc>(
         'CoTaskMemAlloc');
 
-typedef WinCoTaskMemFreeNative = Void Function(Pointer pv);
-typedef WinCoTaskMemFree = void Function(Pointer pv);
-final WinCoTaskMemFree winCoTaskMemFree = stdlib
-    .lookupFunction<WinCoTaskMemFreeNative, WinCoTaskMemFree>('CoTaskMemFree');
+typedef WinCoTaskMemFreeNative = Void Function(Pointer);
+typedef WinCoTaskMemFree = void Function(Pointer);
+final Pointer<NativeFunction<WinCoTaskMemFreeNative>> winCoTaskMemFreePointer =
+    stdlib.lookup('CoTaskMemFree');
+final WinCoTaskMemFree winCoTaskMemFree = winCoTaskMemFreePointer.asFunction();
 
 /// Manages memory on the native heap.
 ///
@@ -43,8 +45,8 @@
 ///
 /// For POSIX-based systems, this uses `malloc` and `free`. On Windows, it uses
 /// `CoTaskMemAlloc`.
-class _MallocAllocator implements Allocator {
-  const _MallocAllocator();
+final class MallocAllocator implements Allocator {
+  const MallocAllocator._();
 
   /// Allocates [byteCount] bytes of of unitialized memory on the native heap.
   ///
@@ -81,6 +83,37 @@
       posixFree(pointer);
     }
   }
+
+  /// Returns a pointer to a native free function.
+  ///
+  /// This function can be used to release memory allocated by [allocated]
+  /// from the native side. It can also be used as a finalization callback
+  /// passed to `NativeFinalizer` constructor or `Pointer.atTypedList`
+  /// method.
+  ///
+  /// For example to automatically free native memory when the Dart object
+  /// wrapping it is reclaimed by GC:
+  ///
+  /// ```dart
+  /// class Wrapper implements Finalizable {
+  ///   static final finalizer = NativeFinalizer(malloc.nativeFree);
+  ///
+  ///   final Pointer<Uint8> data;
+  ///
+  ///   Wrapper() : data = malloc.allocate<Uint8>(length) {
+  ///     finalizer.attach(this, data);
+  ///   }
+  /// }
+  /// ```
+  ///
+  /// or to free native memory that is owned by a typed list:
+  ///
+  /// ```dart
+  /// malloc.allocate<Uint8>(n).asTypedList(n, finalizer: malloc.nativeFree)
+  /// ```
+  ///
+  Pointer<NativeFinalizerFunction> get nativeFree =>
+      Platform.isWindows ? winCoTaskMemFreePointer : posixFreePointer;
 }
 
 /// Manages memory on the native heap.
@@ -90,7 +123,7 @@
 ///
 /// For POSIX-based systems, this uses `malloc` and `free`. On Windows, it uses
 /// `CoTaskMemAlloc` and `CoTaskMemFree`.
-const Allocator malloc = _MallocAllocator();
+const MallocAllocator malloc = MallocAllocator._();
 
 /// Manages memory on the native heap.
 ///
@@ -98,8 +131,8 @@
 ///
 /// For POSIX-based systems, this uses `calloc` and `free`. On Windows, it uses
 /// `CoTaskMemAlloc` and `CoTaskMemFree`.
-class _CallocAllocator implements Allocator {
-  const _CallocAllocator();
+final class CallocAllocator implements Allocator {
+  const CallocAllocator._();
 
   /// Fills a block of memory with a specified value.
   void _fillMemory(Pointer destination, int length, int fill) {
@@ -153,6 +186,37 @@
       posixFree(pointer);
     }
   }
+
+  /// Returns a pointer to a native free function.
+  ///
+  /// This function can be used to release memory allocated by [allocated]
+  /// from the native side. It can also be used as a finalization callback
+  /// passed to `NativeFinalizer` constructor or `Pointer.atTypedList`
+  /// method.
+  ///
+  /// For example to automatically free native memory when the Dart object
+  /// wrapping it is reclaimed by GC:
+  ///
+  /// ```dart
+  /// class Wrapper implements Finalizable {
+  ///   static final finalizer = NativeFinalizer(calloc.nativeFree);
+  ///
+  ///   final Pointer<Uint8> data;
+  ///
+  ///   Wrapper() : data = calloc.allocate<Uint8>(length) {
+  ///     finalizer.attach(this, data);
+  ///   }
+  /// }
+  /// ```
+  ///
+  /// or to free native memory that is owned by a typed list:
+  ///
+  /// ```dart
+  /// calloc.allocate<Uint8>(n).asTypedList(n, finalizer: calloc.nativeFree)
+  /// ```
+  ///
+  Pointer<NativeFinalizerFunction> get nativeFree =>
+      Platform.isWindows ? winCoTaskMemFreePointer : posixFreePointer;
 }
 
 /// Manages memory on the native heap.
@@ -162,4 +226,4 @@
 ///
 /// For POSIX-based systems, this uses `calloc` and `free`. On Windows, it uses
 /// `CoTaskMemAlloc` and `CoTaskMemFree`.
-const Allocator calloc = _CallocAllocator();
+const CallocAllocator calloc = CallocAllocator._();
diff --git a/lib/src/utf16.dart b/lib/src/utf16.dart
index 25c22ed..e5b3309 100644
--- a/lib/src/utf16.dart
+++ b/lib/src/utf16.dart
@@ -13,7 +13,7 @@
 /// through a `Pointer<Utf16>` representing the entire array. This pointer is
 /// the equivalent of a char pointer (`const wchar_t*`) in C code. The
 /// individual UTF-16 code units are stored in native byte order.
-class Utf16 extends Opaque {}
+final class Utf16 extends Opaque {}
 
 /// Extension method for converting a`Pointer<Utf16>` to a [String].
 extension Utf16Pointer on Pointer<Utf16> {
diff --git a/lib/src/utf8.dart b/lib/src/utf8.dart
index 49c1cd3..cdf0e7e 100644
--- a/lib/src/utf8.dart
+++ b/lib/src/utf8.dart
@@ -13,7 +13,7 @@
 /// The Utf8 type itself has no functionality, it's only intended to be used
 /// through a `Pointer<Utf8>` representing the entire array. This pointer is
 /// the equivalent of a char pointer (`const char*`) in C code.
-class Utf8 extends Opaque {}
+final class Utf8 extends Opaque {}
 
 /// Extension method for converting a`Pointer<Utf8>` to a [String].
 extension Utf8Pointer on Pointer<Utf8> {
diff --git a/pubspec.yaml b/pubspec.yaml
index ac21a95..956620e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: ffi
-version: 2.0.2
+version: 2.1.0
 description: Utilities for working with Foreign Function Interface (FFI) code.
 repository: https://github.com/dart-lang/ffi
 
@@ -9,7 +9,7 @@
  - codegen
 
 environment:
-  sdk: '>=2.17.0 <4.0.0'
+  sdk: '>=3.0.0 <4.0.0'
 
 dev_dependencies:
   test: ^1.21.2
diff --git a/test/allocation_test.dart b/test/allocation_test.dart
index 3fbb82d..899b801 100644
--- a/test/allocation_test.dart
+++ b/test/allocation_test.dart
@@ -38,4 +38,13 @@
     // amount of addressable memory on the system.
     expect(() => calloc<Uint8>(-1), throwsA(isA<ArgumentError>()));
   });
+
+  test('nativeFree', () {
+    // malloc.nativeFree should be able to free memory allocated by malloc.
+    final ptr1 = malloc.allocate<Uint8>(1024);
+    malloc.nativeFree.asFunction<void Function(Pointer<Void>)>()(ptr1.cast());
+    // calloc.nativeFree should be able to free memory allocated by calloc.
+    final ptr2 = calloc.allocate<Uint8>(1024);
+    calloc.nativeFree.asFunction<void Function(Pointer<Void>)>()(ptr2.cast());
+  });
 }