Change Utf8 and Ut16 interfaces to extension methods (#83)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6135f4..3e69db0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## 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.
diff --git a/example/main.dart b/example/main.dart
index 7e25bf4..27623f0 100644
--- a/example/main.dart
+++ b/example/main.dart
@@ -11,8 +11,8 @@
 
   // Use the Utf8 helper to encode zero-terminated UTF-8 strings in native memory.
   final String myString = 'πŸ˜ŽπŸ‘ΏπŸ’¬';
-  final Pointer<Utf8> charPointer = Utf8.toUtf8(myString);
+  final Pointer<Utf8> charPointer = myString.toNativeUtf8();
   print('First byte is: ${charPointer.cast<Uint8>().value}');
-  print(Utf8.fromUtf8(charPointer));
+  print(charPointer.toDartString());
   calloc.free(charPointer);
 }
diff --git a/lib/src/utf16.dart b/lib/src/utf16.dart
index fb35b3a..468e32a 100644
--- a/lib/src/utf16.dart
+++ b/lib/src/utf16.dart
@@ -7,24 +7,88 @@
 
 import 'package:ffi/ffi.dart';
 
-/// [Utf16] implements conversion between Dart strings and zero-terminated
-/// UTF-16 encoded "char*" strings in C.
+/// The contents of a native zero-terminated array of UTF-16 code units.
 ///
-/// [Utf16] is represented as a struct so that `Pointer<Utf16>` can be used in
-/// native function signatures.
+/// 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 {
-  /// Convert a [String] to a UTF-16 encoded zero-terminated C string.
+  /// Creates a zero-terminated [Utf16] code-unit array from [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.
+  /// If [string] contains NUL characters, the converted string will be truncated
+  /// prematurely.
   ///
-  /// Returns a [allocator]-allocated pointer to the result.
+  /// Returns an [allocator]-allocated pointer to the result.
+  @Deprecated('Use StringUtf16Pointer.toNativeUtf16 instead.')
   static Pointer<Utf16> toUtf16(String string, {Allocator allocator = calloc}) {
-    final units = string.codeUnits;
+    return string.toNativeUtf16(allocator: allocator);
+  }
+}
+
+/// 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.
+  ///
+  /// 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.
+  ///
+  /// 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 02a5eaa..4683d3e 100644
--- a/lib/src/utf8.dart
+++ b/lib/src/utf8.dart
@@ -8,19 +8,56 @@
 
 import 'package:ffi/ffi.dart';
 
-/// [Utf8] implements conversion between Dart strings and zero-terminated
-/// UTF-8 encoded "char*" strings in C.
+/// The contents of a native zero-terminated array of UTF-8 code units.
 ///
-/// [Utf8] is represented 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.
+/// 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 {
-  /// Returns the length of a zero-terminated string &mdash; the number of
-  /// bytes before the first zero byte.
+  /// 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 bytes up to the
+  /// first zero byte.
+  @Deprecated('Use Utf8Pointer.length instead.')
   static int strlen(Pointer<Utf8> string) {
-    final Pointer<Uint8> array = string.cast<Uint8>();
+    return string.length;
+  }
+
+  /// Converts the UTF-8 encoded [string] to a Dart string.
+  ///
+  /// 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.
+  ///
+  /// If [length] is provided, zero-termination is ignored and the result can
+  /// contain NUL characters.
+  @Deprecated('Use Utf8Pointer.toDartString instead.')
+  static String fromUtf8(Pointer<Utf8> string, {int? length}) {
+    return string.toDartString(length: length);
+  }
+
+  /// Creates a zero-terminated [Utf8] code-unit array from [string].
+  ///
+  /// If [string] contains NUL characters, the converted string will be truncated
+  /// prematurely. Unpaired surrogate code points in [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.
+  @Deprecated('Use StringUtf8Pointer.toNativeUtf8 instead.')
+  static Pointer<Utf8> toUtf8(String string, {Allocator allocator = calloc}) {
+    return string.toNativeUtf8(allocator: allocator);
+  }
+}
+
+/// 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++;
@@ -28,34 +65,39 @@
     return length;
   }
 
-  /// Creates a [String] containing the characters UTF-8 encoded in [string].
+  /// Converts this UTF-8 encoded string to a Dart string.
   ///
-  /// Either the [string] must be zero-terminated or its [length] &mdash; the
-  /// number of bytes &mdash; must be specified as a non-negative value. The
-  /// byte sequence must be valid UTF-8 encodings of Unicode scalar values. A
-  /// [FormatException] is thrown if the input is malformed. 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, {int? 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 = strlen(string);
+      length = this.length;
     }
-    return utf8.decode(string.cast<Uint8>().asTypedList(length));
+    return utf8.decode(cast<Uint8>().asTypedList(length));
   }
+}
 
-  /// Convert a [String] to a UTF-8 encoded zero-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 characters, the converted string will be truncated
-  /// prematurely. Unpaired surrogate code points in [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.
+  /// 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 [allocator]-allocated pointer to the result.
-  static Pointer<Utf8> toUtf8(String string, {Allocator allocator = calloc}) {
-    final units = utf8.encode(string);
+  /// 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);
diff --git a/pubspec.yaml b/pubspec.yaml
index afd19d6..e8a388c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: ffi
-version: 0.3.0-nullsafety.3
+version: 0.3.1-nullsafety.0
 homepage: https://github.com/dart-lang/ffi
 description: Utilities for working with Foreign Function Interface (FFI) code.
 
diff --git a/test/utf16_test.dart b/test/utf16_test.dart
index f0935a1..615056a 100644
--- a/test/utf16_test.dart
+++ b/test/utf16_test.dart
@@ -11,7 +11,7 @@
 void main() {
   test('toUtf16 ASCII', () {
     final String start = 'Hello World!\n';
-    final Pointer<Uint16> converted = Utf16.toUtf16(start).cast();
+    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);
@@ -20,11 +20,47 @@
 
   test('toUtf16 emoji', () {
     final String start = '😎';
-    final Pointer<Utf16> converted = Utf16.toUtf16(start).cast();
+    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);
     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 d4fc8db..9c0486a 100644
--- a/test/utf8_test.dart
+++ b/test/utf8_test.dart
@@ -18,7 +18,7 @@
 void main() {
   test('toUtf8 ASCII', () {
     final String start = 'Hello World!\n';
-    final Pointer<Uint8> converted = Utf8.toUtf8(start).cast();
+    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]);
@@ -29,14 +29,14 @@
   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);
+    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);
+    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]);
@@ -47,46 +47,53 @@
   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);
+    final String end = utf8.toDartString();
     expect(end, 'πŸ˜ŽπŸ‘ΏπŸ’¬');
   });
 
   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.fromUtf8(utf8, length: 5);
+    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.fromUtf8(utf8, length: 4);
+    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.fromUtf8(utf8, length: 0);
+    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.fromUtf8(utf8, length: -1), throwsRangeError);
+    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.fromUtf8(utf8, length: 13);
+    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);
+  });
 }