0.3.0-dev.0 Allocator and Opaque (#72)

Changes `Utf8` and `Utf16` to extend `Opaque` instead of `Struct`.
This means `.ref` is no longer available and `Pointer<Utf(..)>` should be used.
See [breaking change #44622](https://github.com/dart-lang/sdk/issues/44622) for more info.

Removes `allocate` and `free`.
Instead, introduces `calloc` which implements the new `Allocator` interface.
See [breaking change #44621](https://github.com/dart-lang/sdk/issues/44621) for more info.

This pre-release requires Dart `2.12.0-265.0.dev` or greater.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47849bd..ba90e5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
 # Changelog
 
+## 0.3.0-nullsafety.0
+
+Changes `Utf8` and `Utf16` to extend `Opaque` instead of `Struct`.
+This means `.ref` is no longer available and `Pointer<Utf(..)>` should be used.
+See [breaking change #44622](https://github.com/dart-lang/sdk/issues/44622) for more info.
+
+Removes `allocate` and `free`.
+Instead, introduces `calloc` which implements the new `Allocator` interface.
+See [breaking change #44621](https://github.com/dart-lang/sdk/issues/44621) for more info.
+
+This pre-release requires Dart `2.12.0-265.0.dev` or greater.
+
 ## 0.2.0-nullsafety.1
 
 Adds an optional named `length` argument to `Utf8.fromUtf8()`.
diff --git a/example/main.dart b/example/main.dart
index b00ccbb..7e25bf4 100644
--- a/example/main.dart
+++ b/example/main.dart
@@ -3,16 +3,16 @@
 import 'package:ffi/ffi.dart';
 
 void main() {
-  // Allocate and free some native memory with malloc and free.
-  final pointer = allocate<Uint8>();
+  // Allocate and free some native memory with calloc and free.
+  final pointer = calloc<Uint8>();
   pointer.value = 3;
   print(pointer.value);
-  free(pointer);
+  calloc.free(pointer);
 
   // Use the Utf8 helper to encode zero-terminated UTF-8 strings in native memory.
   final String myString = 'πŸ˜ŽπŸ‘ΏπŸ’¬';
   final Pointer<Utf8> charPointer = Utf8.toUtf8(myString);
   print('First byte is: ${charPointer.cast<Uint8>().value}');
   print(Utf8.fromUtf8(charPointer));
-  free(charPointer);
+  calloc.free(charPointer);
 }
diff --git a/lib/ffi.dart b/lib/ffi.dart
index 4f7e546..661a27b 100644
--- a/lib/ffi.dart
+++ b/lib/ffi.dart
@@ -4,4 +4,4 @@
 
 export 'src/utf8.dart';
 export 'src/utf16.dart';
-export 'src/allocation.dart' show allocate, free;
+export 'src/allocation.dart' show calloc, malloc;
diff --git a/lib/src/allocation.dart b/lib/src/allocation.dart
index 9963760..e27e64e 100644
--- a/lib/src/allocation.dart
+++ b/lib/src/allocation.dart
@@ -15,6 +15,11 @@
 final PosixMalloc posixMalloc =
     stdlib.lookupFunction<PosixMallocNative, PosixMalloc>('malloc');
 
+typedef PosixCallocNative = Pointer Function(IntPtr num, IntPtr size);
+typedef PosixCalloc = Pointer Function(int num, int size);
+final PosixCalloc posixCalloc =
+    stdlib.lookupFunction<PosixCallocNative, PosixCalloc>('calloc');
+
 typedef PosixFreeNative = Void Function(Pointer);
 typedef PosixFree = void Function(Pointer);
 final PosixFree posixFree =
@@ -36,43 +41,135 @@
 final WinHeapFree winHeapFree =
     stdlib.lookupFunction<WinHeapFreeNative, WinHeapFree>('HeapFree');
 
-/// Allocates memory on the native heap.
+const int HEAP_ZERO_MEMORY = 8;
+
+/// Manages memory on the native heap.
 ///
-/// For POSIX-based systems, this uses malloc. On Windows, it uses HeapAlloc
-/// against the default public heap. Allocation of either element size or count
-/// of 0 is undefined.
+/// Does not initialize newly allocated memory to zero. Use [_CallocAllocator]
+/// for zero-initialized memory on allocation.
 ///
-/// Throws an ArgumentError on failure to allocate.
-Pointer<T> allocate<T extends NativeType>({int count = 1}) {
-  final int totalSize = count * sizeOf<T>();
-  Pointer<T> result;
-  if (Platform.isWindows) {
-    result = winHeapAlloc(processHeap, /*flags=*/ 0, totalSize).cast();
-  } else {
-    result = posixMalloc(totalSize).cast();
+/// For POSIX-based systems, this uses `malloc` and `free`. On Windows, it uses
+/// `HeapAlloc` and `HeapFree` against the default public heap.
+class _MallocAllocator implements Allocator {
+  const _MallocAllocator();
+
+  /// Allocates [byteCount] bytes of of unitialized memory on the native heap.
+  ///
+  /// For POSIX-based systems, this uses `malloc`. On Windows, it uses
+  /// `HeapAlloc` against the default public heap.
+  ///
+  /// Throws an [ArgumentError] if the number of bytes or alignment cannot be
+  /// satisfied.
+  // TODO: Stop ignoring alignment if it's large, for example for SSE data.
+  @override
+  Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment}) {
+    Pointer<T> result;
+    if (Platform.isWindows) {
+      result = winHeapAlloc(processHeap, /*flags=*/ 0, byteCount).cast();
+    } else {
+      result = posixMalloc(byteCount).cast();
+    }
+    if (result.address == 0) {
+      throw ArgumentError('Could not allocate $byteCount bytes.');
+    }
+    return result;
   }
-  if (result.address == 0) {
-    throw ArgumentError('Could not allocate $totalSize bytes.');
+
+  /// Releases memory allocated on the native heap.
+  ///
+  /// For POSIX-based systems, this uses `free`. On Windows, it uses `HeapFree`
+  /// against the default public heap. It may only be used against pointers
+  /// allocated in a manner equivalent to [allocate].
+  ///
+  /// Throws an [ArgumentError] if the memory pointed to by [pointer] cannot be
+  /// freed.
+  ///
+  // TODO(dartbug.com/36855): Once we have a ffi.Bool type we can use it instead
+  // of testing the return integer to be non-zero.
+  @override
+  void free(Pointer pointer) {
+    if (Platform.isWindows) {
+      if (winHeapFree(processHeap, /*flags=*/ 0, pointer) == 0) {
+        throw ArgumentError('Could not free $pointer.');
+      }
+    } else {
+      posixFree(pointer);
+    }
   }
-  return result;
 }
 
-/// Releases memory on the native heap.
+/// Manages memory on the native heap.
 ///
-/// For POSIX-based systems, this uses free. On Windows, it uses HeapFree
-/// against the default public heap. It may only be used against pointers
-/// allocated in a manner equivalent to [allocate].
+/// Does not initialize newly allocated memory to zero. Use [calloc] for
+/// zero-initialized memory allocation.
 ///
-/// Throws an ArgumentError on failure to free.
+/// For POSIX-based systems, this uses `malloc` and `free`. On Windows, it uses
+/// `HeapAlloc` and `HeapFree` against the default public heap.
+const Allocator malloc = _MallocAllocator();
+
+/// Manages memory on the native heap.
 ///
-// TODO(dartbug.com/36855): Once we have a ffi.Bool type we can use it instead
-// of testing the return integer to be non-zero.
-void free(Pointer pointer) {
-  if (Platform.isWindows) {
-    if (winHeapFree(processHeap, /*flags=*/ 0, pointer) == 0) {
-      throw ArgumentError('Could not free $pointer.');
+/// Initializes newly allocated memory to zero.
+///
+/// For POSIX-based systems, this uses `calloc` and `free`. On Windows, it uses
+/// `HeapAlloc` with [HEAP_ZERO_MEMORY] and `HeapFree` against the default
+/// public heap.
+class _CallocAllocator implements Allocator {
+  const _CallocAllocator();
+
+  /// Allocates [byteCount] bytes of zero-initialized of memory on the native
+  /// heap.
+  ///
+  /// For POSIX-based systems, this uses `malloc`. On Windows, it uses
+  /// `HeapAlloc` against the default public heap.
+  ///
+  /// Throws an [ArgumentError] if the number of bytes or alignment cannot be
+  /// satisfied.
+  // TODO: Stop ignoring alignment if it's large, for example for SSE data.
+  @override
+  Pointer<T> allocate<T extends NativeType>(int byteCount, {int? alignment}) {
+    Pointer<T> result;
+    if (Platform.isWindows) {
+      result = winHeapAlloc(processHeap, /*flags=*/ HEAP_ZERO_MEMORY, byteCount)
+          .cast();
+    } else {
+      result = posixCalloc(byteCount, 1).cast();
     }
-  } else {
-    posixFree(pointer);
+    if (result.address == 0) {
+      throw ArgumentError('Could not allocate $byteCount bytes.');
+    }
+    return result;
+  }
+
+  /// Releases memory allocated on the native heap.
+  ///
+  /// For POSIX-based systems, this uses `free`. On Windows, it uses `HeapFree`
+  /// against the default public heap. It may only be used against pointers
+  /// allocated in a manner equivalent to [allocate].
+  ///
+  /// Throws an [ArgumentError] if the memory pointed to by [pointer] cannot be
+  /// freed.
+  ///
+  // TODO(dartbug.com/36855): Once we have a ffi.Bool type we can use it instead
+  // of testing the return integer to be non-zero.
+  @override
+  void free(Pointer pointer) {
+    if (Platform.isWindows) {
+      if (winHeapFree(processHeap, /*flags=*/ 0, pointer) == 0) {
+        throw ArgumentError('Could not free $pointer.');
+      }
+    } else {
+      posixFree(pointer);
+    }
   }
 }
+
+/// Manages memory on the native heap.
+///
+/// Initializes newly allocated memory to zero. Use [malloc] for uninitialized
+/// memory allocation.
+///
+/// For POSIX-based systems, this uses `calloc` and `free`. On Windows, it uses
+/// `HeapAlloc` with [HEAP_ZERO_MEMORY] and `HeapFree` against the default
+/// public heap.
+const Allocator calloc = _CallocAllocator();
diff --git a/lib/src/utf16.dart b/lib/src/utf16.dart
index b63d383..fb35b3a 100644
--- a/lib/src/utf16.dart
+++ b/lib/src/utf16.dart
@@ -12,17 +12,17 @@
 ///
 /// [Utf16] is represented as a struct so that `Pointer<Utf16>` can be used in
 /// native function signatures.
-class Utf16 extends Struct {
+class Utf16 extends Opaque {
   /// Convert a [String] to a UTF-16 encoded zero-terminated C string.
   ///
   /// If [string] contains NULL characters, the converted string will be truncated
   /// prematurely. Unpaired surrogate code points in [string] will be preserved
   /// in the UTF-16 encoded result. See [Utf16Encoder] for details on encoding.
   ///
-  /// Returns a malloc-allocated pointer to the result.
-  static Pointer<Utf16> toUtf16(String string) {
+  /// Returns a [allocator]-allocated pointer to the result.
+  static Pointer<Utf16> toUtf16(String string, {Allocator allocator = calloc}) {
     final units = string.codeUnits;
-    final Pointer<Uint16> result = allocate<Uint16>(count: units.length + 1);
+    final Pointer<Uint16> result = allocator<Uint16>(units.length + 1);
     final Uint16List nativeString = result.asTypedList(units.length + 1);
     nativeString.setAll(0, units);
     nativeString[units.length] = 0;
diff --git a/lib/src/utf8.dart b/lib/src/utf8.dart
index cf44f06..3360922 100644
--- a/lib/src/utf8.dart
+++ b/lib/src/utf8.dart
@@ -20,7 +20,7 @@
 //
 // TODO(https://github.com/dart-lang/ffi/issues/4): No need to use
 // 'asTypedList' when Pointer operations are performant.
-class Utf8 extends Struct {
+class Utf8 extends Opaque {
   /// Returns the length of a zero-terminated string &mdash; the number of
   /// bytes before the first zero byte.
   static int strlen(Pointer<Utf8> string) {
@@ -54,16 +54,13 @@
   /// as replacement characters (U+FFFD, encoded as the bytes 0xEF 0xBF 0xBD)
   /// in the UTF-8 encoded result. See [Utf8Encoder] for details on encoding.
   ///
-  /// Returns a malloc-allocated pointer to the result.
-  static Pointer<Utf8> toUtf8(String string) {
+  /// Returns a [allocator]-allocated pointer to the result.
+  static Pointer<Utf8> toUtf8(String string, {Allocator allocator = calloc}) {
     final units = utf8.encode(string);
-    final Pointer<Uint8> result = allocate<Uint8>(count: units.length + 1);
+    final Pointer<Uint8> result = allocator<Uint8>(units.length + 1);
     final Uint8List nativeString = result.asTypedList(units.length + 1);
     nativeString.setAll(0, units);
     nativeString[units.length] = 0;
     return result.cast();
   }
-
-  @override
-  String toString() => fromUtf8(addressOf);
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index 5d7af25..5a91bb9 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,10 +1,10 @@
 name: ffi
-version: 0.2.0-nullsafety.1
+version: 0.3.0-nullsafety.0
 homepage: https://github.com/dart-lang/ffi
 description: Utilities for working with Foreign Function Interface (FFI) code.
 
 environment:
-  sdk: '>=2.12.0-0 <3.0.0'
+  sdk: '>=2.12.0-265.0.dev <3.0.0'
 
 # dependencies:
 
diff --git a/test/utf16_test.dart b/test/utf16_test.dart
index db1e361..f0935a1 100644
--- a/test/utf16_test.dart
+++ b/test/utf16_test.dart
@@ -15,7 +15,7 @@
     final Uint16List end = converted.asTypedList(start.codeUnits.length + 1);
     final matcher = equals(start.codeUnits.toList()..add(0));
     expect(end, matcher);
-    free(converted);
+    calloc.free(converted);
   });
 
   test('toUtf16 emoji', () {
@@ -25,6 +25,6 @@
     final Uint16List end = converted.cast<Uint16>().asTypedList(length + 1);
     final matcher = equals(start.codeUnits.toList()..add(0));
     expect(end, matcher);
-    free(converted);
+    calloc.free(converted);
   });
 }
diff --git a/test/utf8_test.dart b/test/utf8_test.dart
index d8d91ec..d4fc8db 100644
--- a/test/utf8_test.dart
+++ b/test/utf8_test.dart
@@ -9,7 +9,7 @@
 import 'package:test/test.dart';
 
 Pointer<Uint8> _bytesFromList(List<int> ints) {
-  final Pointer<Uint8> ptr = allocate(count: ints.length);
+  final Pointer<Uint8> ptr = calloc(ints.length);
   final Uint8List list = ptr.asTypedList(ints.length);
   list.setAll(0, ints);
   return ptr;
@@ -23,7 +23,7 @@
     final matcher =
         equals([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 10, 0]);
     expect(end, matcher);
-    free(converted);
+    calloc.free(converted);
   });
 
   test('fromUtf8 ASCII', () {
@@ -41,7 +41,7 @@
     final matcher =
         equals([240, 159, 152, 142, 240, 159, 145, 191, 240, 159, 146, 172, 0]);
     expect(end, matcher);
-    free(converted);
+    calloc.free(converted);
   });
 
   test('formUtf8 emoji', () {