Merge commit '454ab0f' to master

This commit was used in the DEPS for a while, but was lost when the
branch was deleted.

See https://github.com/flutter/flutter/issues/76952.
diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
new file mode 100644
index 0000000..1cd8438
--- /dev/null
+++ b/.github/workflows/test-package.yml
@@ -0,0 +1,61 @@
+name: Dart CI
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+jobs:
+  # Check code formatting and static analysis on a single OS (linux)
+  # against Dart dev.
+  analyze:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        sdk: [dev]
+    steps:
+      - uses: actions/checkout@v2
+      - uses: dart-lang/setup-dart@v0.1
+        with:
+          channel: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Check formatting
+        run: dart format --output=none --set-exit-if-changed .
+        if: always() && steps.install.outcome == 'success'
+      - name: Analyze code
+        run: dart analyze --fatal-infos
+        if: always() && steps.install.outcome == 'success'
+
+  # Run tests on a matrix consisting of two dimensions:
+  # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+  # 2. release channel: dev
+  test:
+    needs: analyze
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        # Add macos-latest and/or windows-latest if relevant for this package.
+        os: [ubuntu-latest]
+        sdk: [dev]
+    steps:
+      - uses: actions/checkout@v2
+      - uses: dart-lang/setup-dart@v0.1
+        with:
+          channel: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Run VM tests
+        run: dart test --platform vm
+        if: always() && steps.install.outcome == 'success'
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 6ec14e5..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-language: dart
-dart:
-- dev
-# Only building master means that we don't run two builds for each pull request.
-dart_task:
-- test: --platform vm
-- dartanalyzer
-- dartfmt
-branches:
-  only: [master]
-cache:
-  directories:
-    - $HOME/.pub-cache
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d335e8..8d404e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,53 @@
 # Changelog
 
+## 1.0.0
+
+Bumping the version of this package to `1.0.0`.
+
+Removes all deprecated methods, use `0.3.0-nullsafety.3` for migration.
+
+## 0.3.1-nullsafety.0
+
+Deprecates the static methods on `Utf8` and `Utf16` and introduces
+extension methods to replace them.
+
+## 0.3.0-nullsafety.3
+
+Adds back in deprecated `allocate` and `free` to ease migration.
+These will be removed in the next release.
+
+This pre-release requires Dart `2.12.0-259.9.beta` or greater.
+
+## 0.3.0-nullsafety.1
+
+This pre-release requires Dart `2.12.0-259.8.beta` or greater.
+
+Note that this pre-release does _not_ work in Flutter versions containing Dart
+`2.12.0-260.0.dev` - `2.12.0-264.0.dev`.
+Using `Allocator.call` throws a `NoSuchMethodError` in these versions.
+See [Flutter Engine #23954](https://github.com/flutter/engine/pull/23954) for more info.
+
+## 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()`.
+
+## 0.2.0-nullsafety.0
+
+Pre-release (non-stable) release supporting null safety.
+Requires Dart 2.12.0 or greater.
+
 ## 0.1.3
 
 Stable release incorporating all the previous dev release changes.
diff --git a/README.md b/README.md
index 48e5873..63c3dd3 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-[![Build Status](https://travis-ci.org/dart-lang/ffi.svg?branch=master)](https://travis-ci.org/dart-lang/ffi)
+[![Build Status](https://github.com/dart-lang/ffi/workflows/Dart%20CI/badge.svg)](https://github.com/dart-lang/ffi/actions?query=workflow%3A"Dart+CI")
 
 Utilities for working with Foreign Function Interface (FFI) code, incl.
-converting between Dart strings and C strings encoded with utf8 and utf16.
+converting between Dart strings and C strings encoded with UTF-8 and UTF-16.
 
 For additional details about Dart FFI (`dart:ffi`), see
 https://dart.dev/guides/libraries/c-interop.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 108d105..a5b0738 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1 +1,5 @@
 include: package:pedantic/analysis_options.yaml
+
+linter:
+  rules:
+    omit_local_variable_types: false
diff --git a/example/main.dart b/example/main.dart
index 352f834..27623f0 100644
--- a/example/main.dart
+++ b/example/main.dart
@@ -2,17 +2,17 @@
 
 import 'package:ffi/ffi.dart';
 
-main() {
-  // Allocate and free some native memory with malloc and free.
-  final pointer = allocate<Uint8>();
+void main() {
+  // 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 null-terminated Utf8 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);
+  // Use the Utf8 helper to encode zero-terminated UTF-8 strings in native memory.
+  final String myString = 'πŸ˜ŽπŸ‘ΏπŸ’¬';
+  final Pointer<Utf8> charPointer = myString.toNativeUtf8();
+  print('First byte is: ${charPointer.cast<Uint8>().value}');
+  print(charPointer.toDartString());
+  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 c1b7277..e27e64e 100644
--- a/lib/src/allocation.dart
+++ b/lib/src/allocation.dart
@@ -7,72 +7,169 @@
 
 // Note that kernel32.dll is the correct name in both 32-bit and 64-bit.
 final DynamicLibrary stdlib = Platform.isWindows
-    ? DynamicLibrary.open("kernel32.dll")
+    ? DynamicLibrary.open('kernel32.dll')
     : DynamicLibrary.process();
 
 typedef PosixMallocNative = Pointer Function(IntPtr);
 typedef PosixMalloc = Pointer Function(int);
 final PosixMalloc posixMalloc =
-    stdlib.lookupFunction<PosixMallocNative, PosixMalloc>("malloc");
+    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 =
-    stdlib.lookupFunction<PosixFreeNative, PosixFree>("free");
+    stdlib.lookupFunction<PosixFreeNative, PosixFree>('free');
 
 typedef WinGetProcessHeapFn = Pointer Function();
 final WinGetProcessHeapFn winGetProcessHeap = stdlib
-    .lookupFunction<WinGetProcessHeapFn, WinGetProcessHeapFn>("GetProcessHeap");
+    .lookupFunction<WinGetProcessHeapFn, WinGetProcessHeapFn>('GetProcessHeap');
 final Pointer processHeap = winGetProcessHeap();
 
 typedef WinHeapAllocNative = Pointer Function(Pointer, Uint32, IntPtr);
 typedef WinHeapAlloc = Pointer Function(Pointer, int, int);
 final WinHeapAlloc winHeapAlloc =
-    stdlib.lookupFunction<WinHeapAllocNative, WinHeapAlloc>("HeapAlloc");
+    stdlib.lookupFunction<WinHeapAllocNative, WinHeapAlloc>('HeapAlloc');
 
 typedef WinHeapFreeNative = Int32 Function(
     Pointer heap, Uint32 flags, Pointer memory);
 typedef WinHeapFree = int Function(Pointer heap, int flags, Pointer memory);
 final WinHeapFree winHeapFree =
-    stdlib.lookupFunction<WinHeapFreeNative, WinHeapFree>("HeapFree");
+    stdlib.lookupFunction<WinHeapFreeNative, WinHeapFree>('HeapFree');
 
-/// Allocates 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.
-///
-/// 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();
-  }
-  if (result.address == 0) {
-    throw ArgumentError("Could not allocate $totalSize bytes.");
-  }
-  return result;
-}
+const int HEAP_ZERO_MEMORY = 8;
 
-/// 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 [_CallocAllocator]
+/// for zero-initialized memory on allocation.
 ///
-/// Throws an ArgumentError on failure to free.
-///
-// 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.");
+/// 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();
     }
-  } 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.
+///
+/// Does not initialize newly allocated memory to zero. Use [calloc] for
+/// zero-initialized memory allocation.
+///
+/// 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.
+///
+/// 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();
+    }
+    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 f1b64fa..f5bea5b 100644
--- a/lib/src/utf16.dart
+++ b/lib/src/utf16.dart
@@ -2,30 +2,82 @@
 // 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.
 
-import 'dart:convert';
 import 'dart:ffi';
 import 'dart:typed_data';
 
 import 'package:ffi/ffi.dart';
 
-/// [Utf16] implements conversion between Dart strings and null-terminated
-/// UTF-16 encoded "char*" strings in C.
+/// The contents of a native zero-terminated array of UTF-16 code units.
 ///
-/// [Utf16] is respresented as a struct so that `Pointer<Utf16>` can be used in
-/// native function signatures.
-class Utf16 extends Struct {
-  /// Convert a [String] to a Utf16-encoded null-terminated C string.
+/// The Utf16 type itself has no functionality, it's only intended to be used
+/// 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 {}
+
+/// Extension method for converting a`Pointer<Utf16>` to a [String].
+extension Utf16Pointer on Pointer<Utf16> {
+  /// The number of UTF-16 code units in this zero-terminated UTF-16 string.
   ///
-  /// If 'string' contains NULL bytes, the converted string will be truncated
-  /// prematurely. Unpaired surrogate code points in [string] will be preserved
-  /// in the UTF-8 encoded result. See [Utf8Encoder] for details on encoding.
+  /// The UTF-16 code units of the strings are the non-zero code units up to
+  /// the first zero code unit.
+  int get length {
+    final Pointer<Uint16> array = cast<Uint16>();
+    int length = 0;
+    while (array[length] != 0) {
+      length++;
+    }
+    return length;
+  }
+
+  /// Converts this UTF-16 encoded string to a Dart string.
   ///
-  /// Returns a malloc-allocated pointer to the result.
-  static Pointer<Utf16> toUtf16(String s) {
-    final units = s.codeUnits;
-    final Pointer<Uint16> result = allocate<Uint16>(count: units.length + 1);
+  /// Decodes the UTF-16 code units of this zero-terminated code unit array as
+  /// Unicode code points and creates a Dart string containing those code
+  /// points.
+  ///
+  /// If [length] is provided, zero-termination is ignored and the result can
+  /// contain NUL characters.
+  String toDartString({int? length}) {
+    if (length == null) {
+      return _toUnknownLengthString(cast<Uint16>());
+    } else {
+      RangeError.checkNotNegative(length, 'length');
+      return _toKnownLengthString(cast<Uint16>(), length);
+    }
+  }
+
+  static String _toKnownLengthString(Pointer<Uint16> codeUnits, int length) =>
+      String.fromCharCodes(codeUnits.asTypedList(length));
+
+  static String _toUnknownLengthString(Pointer<Uint16> codeUnits) {
+    final buffer = StringBuffer();
+    var i = 0;
+    while (true) {
+      final char = codeUnits.elementAt(i).value;
+      if (char == 0) {
+        return buffer.toString();
+      }
+      buffer.writeCharCode(char);
+      i++;
+    }
+  }
+}
+
+/// Extension method for converting a [String] to a `Pointer<Utf16>`.
+extension StringUtf16Pointer on String {
+  /// Creates a zero-terminated [Utf16] code-unit array from this String.
+  ///
+  /// If this [String] contains NUL characters, converting it back to a string
+  /// using [Utf16Pointer.toDartString] will truncate the result if a length is
+  /// not passed.
+  ///
+  /// Returns an [allocator]-allocated pointer to the result.
+  Pointer<Utf16> toNativeUtf16({Allocator allocator = malloc}) {
+    final units = codeUnits;
+    final Pointer<Uint16> result = allocator<Uint16>(units.length + 1);
     final Uint16List nativeString = result.asTypedList(units.length + 1);
-    nativeString.setAll(0, units);
+    nativeString.setRange(0, units.length, units);
     nativeString[units.length] = 0;
     return result.cast();
   }
diff --git a/lib/src/utf8.dart b/lib/src/utf8.dart
index df8f1f2..04dd7a7 100644
--- a/lib/src/utf8.dart
+++ b/lib/src/utf8.dart
@@ -8,57 +8,65 @@
 
 import 'package:ffi/ffi.dart';
 
-const int _kMaxSmi64 = (1 << 62) - 1;
-const int _kMaxSmi32 = (1 << 30) - 1;
-final int _maxSize = sizeOf<IntPtr>() == 8 ? _kMaxSmi64 : _kMaxSmi32;
-
-/// [Utf8] implements conversion between Dart strings and null-terminated
-/// Utf8-encoded "char*" strings in C.
+/// The contents of a native zero-terminated array of UTF-8 code units.
 ///
-/// [Utf8] is respresented as a struct so that `Pointer<Utf8>` can be used in
-/// native function signatures.
-//
-// TODO(https://github.com/dart-lang/ffi/issues/4): No need to use
-// 'asTypedList' when Pointer operations are performant.
-class Utf8 extends Struct {
-  /// Returns the length of a null-terminated string -- the number of (one-byte)
-  /// characters before the first null byte.
-  static int strlen(Pointer<Utf8> string) {
-    final Pointer<Uint8> array = string.cast<Uint8>();
-    final Uint8List nativeString = array.asTypedList(_maxSize);
-    return nativeString.indexWhere((char) => char == 0);
+/// 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 {}
+
+/// Extension method for converting a`Pointer<Utf8>` to a [String].
+extension Utf8Pointer on Pointer<Utf8> {
+  /// The number of UTF-8 code units in this zero-terminated UTF-8 string.
+  ///
+  /// The UTF-8 code units of the strings are the non-zero code units up to the
+  /// first zero code unit.
+  int get length {
+    final Pointer<Uint8> array = cast<Uint8>();
+    int length = 0;
+    while (array[length] != 0) {
+      length++;
+    }
+    return length;
   }
 
-  /// Creates a [String] containing the characters UTF-8 encoded in [string].
+  /// Converts this UTF-8 encoded string to a Dart string.
   ///
-  /// The [string] must be a zero-terminated byte sequence of valid UTF-8
-  /// encodings of Unicode code points. It may also contain UTF-8 encodings of
-  /// unpaired surrogate code points, which is not otherwise valid UTF-8, but
-  /// which may be created when encoding a Dart string containing an unpaired
-  /// surrogate. See [Utf8Decoder] for details on decoding.
+  /// Decodes the UTF-8 code units of this zero-terminated byte array as
+  /// Unicode code points and creates a Dart string containing those code
+  /// points.
   ///
-  /// Returns a Dart string containing the decoded code points.
-  static String fromUtf8(Pointer<Utf8> string) {
-    final int length = strlen(string);
-    return utf8.decode(Uint8List.view(
-        string.cast<Uint8>().asTypedList(length).buffer, 0, length));
+  /// If [length] is provided, zero-termination is ignored and the result can
+  /// contain NUL characters.
+  String toDartString({int? length}) {
+    if (length != null) {
+      RangeError.checkNotNegative(length, 'length');
+    } else {
+      length = this.length;
+    }
+    return utf8.decode(cast<Uint8>().asTypedList(length));
   }
+}
 
-  /// Convert a [String] to a Utf8-encoded null-terminated C string.
+/// Extension method for converting a [String] to a `Pointer<Utf8>`.
+extension StringUtf8Pointer on String {
+  /// Creates a zero-terminated [Utf8] code-unit array from this String.
   ///
-  /// If 'string' contains NULL bytes, the converted string will be truncated
-  /// prematurely. Unpaired surrogate code points in [string] will be preserved
-  /// in the UTF-8 encoded result. See [Utf8Encoder] for details on encoding.
+  /// If this [String] contains NUL characters, converting it back to a string
+  /// using [Utf8Pointer.toDartString] will truncate the result if a length is
+  /// not passed.
   ///
-  /// Returns a malloc-allocated pointer to the result.
-  static Pointer<Utf8> toUtf8(String string) {
-    final units = utf8.encode(string);
-    final Pointer<Uint8> result = allocate<Uint8>(count: units.length + 1);
+  /// Unpaired surrogate code points in this [String] will be encoded 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 an [allocator]-allocated pointer to the result.
+  Pointer<Utf8> toNativeUtf8({Allocator allocator = malloc}) {
+    final units = utf8.encode(this);
+    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();
   }
-
-  String toString() => fromUtf8(addressOf);
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index 25a3aeb..d2daa1b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,13 +1,11 @@
 name: ffi
-version: 0.1.3+1
+version: 1.0.0
 homepage: https://github.com/dart-lang/ffi
 description: Utilities for working with Foreign Function Interface (FFI) code.
 
 environment:
-  sdk: '>=2.9.0 <3.0.0'
-
-# dependencies:
+  sdk: '>=2.12.0-259.9.beta <3.0.0'
 
 dev_dependencies:
-  pedantic: ^1.0.0
-  test: ^1.6.8
+  pedantic: ^1.10.0
+  test: ^1.16.0
diff --git a/test/utf16_test.dart b/test/utf16_test.dart
index f55cb3b..615056a 100644
--- a/test/utf16_test.dart
+++ b/test/utf16_test.dart
@@ -5,26 +5,62 @@
 import 'dart:ffi';
 import 'dart:typed_data';
 
-import 'package:test/test.dart';
 import 'package:ffi/ffi.dart';
+import 'package:test/test.dart';
 
-main() {
-  test("toUtf16 ASCII", () {
-    final String start = "Hello World!\n";
-    final Pointer<Uint16> converted = Utf16.toUtf16(start).cast();
+void main() {
+  test('toUtf16 ASCII', () {
+    final String start = 'Hello World!\n';
+    final Pointer<Uint16> converted = start.toNativeUtf16().cast();
     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", () {
-    final String start = "😎";
-    final Pointer<Utf16> converted = Utf16.toUtf16(start).cast();
+  test('toUtf16 emoji', () {
+    final String start = '😎';
+    final Pointer<Utf16> converted = start.toNativeUtf16().cast();
     final int length = start.codeUnits.length;
     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);
+  });
+
+  test('from Utf16 ASCII', () {
+    final string = 'Hello World!\n';
+    final utf16Pointer = string.toNativeUtf16();
+    final stringAgain = utf16Pointer.toDartString();
+    expect(stringAgain, string);
+    calloc.free(utf16Pointer);
+  });
+
+  test('from Utf16 emoji', () {
+    final string = '😎';
+    final utf16Pointer = string.toNativeUtf16();
+    final stringAgain = utf16Pointer.toDartString();
+    expect(stringAgain, string);
+    calloc.free(utf16Pointer);
+  });
+
+  test('zero bytes', () {
+    final string = 'Hello\x00World!\n';
+    final utf16Pointer = string.toNativeUtf16();
+    final stringAgain = utf16Pointer.toDartString(length: 13);
+    expect(stringAgain, string);
+    calloc.free(utf16Pointer);
+  });
+
+  test('length', () {
+    final string = 'Hello';
+    final utf16Pointer = string.toNativeUtf16();
+    expect(utf16Pointer.length, 5);
+    calloc.free(utf16Pointer);
+  });
+
+  test('fromUtf8 with negative length', () {
+    final Pointer<Utf16> utf16 = Pointer.fromAddress(0);
+    expect(() => utf16.toDartString(length: -1), throwsRangeError);
   });
 }
diff --git a/test/utf8_test.dart b/test/utf8_test.dart
index ab673b2..9c0486a 100644
--- a/test/utf8_test.dart
+++ b/test/utf8_test.dart
@@ -5,54 +5,95 @@
 import 'dart:ffi';
 import 'dart:typed_data';
 
-import 'package:test/test.dart';
 import 'package:ffi/ffi.dart';
+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;
 }
 
-main() {
-  test("toUtf8 ASCII", () {
-    final String start = "Hello World!\n";
-    final Pointer<Uint8> converted = Utf8.toUtf8(start).cast();
+void main() {
+  test('toUtf8 ASCII', () {
+    final String start = 'Hello World!\n';
+    final Pointer<Uint8> converted = start.toNativeUtf8().cast();
     final Uint8List end = converted.asTypedList(start.length + 1);
     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", () {
+  test('fromUtf8 ASCII', () {
     final Pointer<Utf8> utf8 = _bytesFromList(
         [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 10, 0]).cast();
-    final String end = Utf8.fromUtf8(utf8);
-    expect(end, "Hello World!\n");
+    final String end = utf8.toDartString();
+    expect(end, 'Hello World!\n');
   });
 
-  test("toUtf8 emoji", () {
-    final String start = "πŸ˜ŽπŸ‘ΏπŸ’¬";
-    final Pointer<Utf8> converted = Utf8.toUtf8(start).cast();
-    final int length = Utf8.strlen(converted);
+  test('toUtf8 emoji', () {
+    final String start = 'πŸ˜ŽπŸ‘ΏπŸ’¬';
+    final Pointer<Utf8> converted = start.toNativeUtf8().cast();
+    final int length = converted.length;
     final Uint8List end = converted.cast<Uint8>().asTypedList(length + 1);
     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", () {
+  test('formUtf8 emoji', () {
     final Pointer<Utf8> utf8 = _bytesFromList(
         [240, 159, 152, 142, 240, 159, 145, 191, 240, 159, 146, 172, 0]).cast();
-    final String end = Utf8.fromUtf8(utf8);
-    expect(end, "πŸ˜ŽπŸ‘ΏπŸ’¬");
+    final String end = utf8.toDartString();
+    expect(end, 'πŸ˜ŽπŸ‘ΏπŸ’¬');
   });
 
-  test("fromUtf8 invalid", () {
+  test('fromUtf8 invalid', () {
     final Pointer<Utf8> utf8 = _bytesFromList([0x80, 0x00]).cast();
-    expect(() => Utf8.fromUtf8(utf8), throwsA(isFormatException));
+    expect(() => utf8.toDartString(), throwsA(isFormatException));
+  });
+
+  test('fromUtf8 ASCII with length', () {
+    final Pointer<Utf8> utf8 = _bytesFromList(
+        [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 10, 0]).cast();
+    final String end = utf8.toDartString(length: 5);
+    expect(end, 'Hello');
+  });
+
+  test('fromUtf8 emoji with length', () {
+    final Pointer<Utf8> utf8 = _bytesFromList(
+        [240, 159, 152, 142, 240, 159, 145, 191, 240, 159, 146, 172, 0]).cast();
+    final String end = utf8.toDartString(length: 4);
+    expect(end, '😎');
+  });
+
+  test('fromUtf8 with zero length', () {
+    final Pointer<Utf8> utf8 = _bytesFromList(
+        [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 10, 0]).cast();
+    final String end = utf8.toDartString(length: 0);
+    expect(end, '');
+  });
+
+  test('fromUtf8 with negative length', () {
+    final Pointer<Utf8> utf8 = _bytesFromList(
+        [72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 10, 0]).cast();
+    expect(() => utf8.toDartString(length: -1), throwsRangeError);
+  });
+
+  test('fromUtf8 with length and containing a zero byte', () {
+    final Pointer<Utf8> utf8 = _bytesFromList(
+        [72, 101, 108, 108, 111, 0, 87, 111, 114, 108, 100, 33, 10]).cast();
+    final String end = utf8.toDartString(length: 13);
+    expect(end, 'Hello\x00World!\n');
+  });
+
+  test('length', () {
+    final string = 'Hello';
+    final utf8Pointer = string.toNativeUtf8();
+    expect(utf8Pointer.length, 5);
+    calloc.free(utf8Pointer);
   });
 }