Integrate google internal changes.
diff --git a/js/binary/constants.js b/js/binary/constants.js
index a976e0b..836216b 100644
--- a/js/binary/constants.js
+++ b/js/binary/constants.js
@@ -41,11 +41,16 @@
 goog.provide('jspb.BuilderFunction');
 goog.provide('jspb.ByteSource');
 goog.provide('jspb.ClonerFunction');
+goog.provide('jspb.ComparerFunction');
 goog.provide('jspb.ConstBinaryMessage');
+goog.provide('jspb.PrunerFunction');
 goog.provide('jspb.ReaderFunction');
 goog.provide('jspb.RecyclerFunction');
+goog.provide('jspb.RepeatedFieldType');
+goog.provide('jspb.ScalarFieldType');
 goog.provide('jspb.WriterFunction');
 
+
 goog.forwardDeclare('jspb.Message');
 goog.forwardDeclare('jsproto.BinaryExtension');
 
@@ -79,11 +84,29 @@
 
 
 /**
+ * A scalar field in jspb can be a boolean, number, or string.
+ * @typedef {boolean|number|string}
+ */
+jspb.ScalarFieldType;
+
+
+/**
+ * A repeated field in jspb is an array of scalars, blobs, or messages.
+ * @typedef {!Array<jspb.ScalarFieldType>|
+             !Array<!Uint8Array>|
+             !Array<!jspb.BinaryMessage>}
+ */
+jspb.RepeatedFieldType;
+
+
+/**
  * A field in jspb can be a scalar, a block of bytes, another proto, or an
  * array of any of the above.
- * @typedef {boolean|number|string|Uint8Array|
-             jspb.BinaryMessage|jsproto.BinaryExtension|
-             Array<jspb.AnyFieldType>}
+ * @typedef {jspb.ScalarFieldType|
+             jspb.RepeatedFieldType|
+             !Uint8Array|
+             !jspb.BinaryMessage|
+             !jsproto.BinaryExtension}
  */
 jspb.AnyFieldType;
 
@@ -125,6 +148,23 @@
 
 
 /**
+ * A pruner function removes default-valued fields and empty submessages from a
+ * message and returns either the pruned message or null if the entire message
+ * was pruned away.
+ * @typedef {function(?jspb.BinaryMessage):?jspb.BinaryMessage}
+ */
+jspb.PrunerFunction;
+
+
+/**
+ * A comparer function returns true if two protos are equal.
+ * @typedef {!function(?jspb.ConstBinaryMessage,
+ *                     ?jspb.ConstBinaryMessage):boolean}
+ */
+jspb.ComparerFunction;
+
+
+/**
  * Field type codes, taken from proto2/public/wire_format_lite.h.
  * @enum {number}
  */
diff --git a/js/binary/decoder.js b/js/binary/decoder.js
index 9004eff..41094a3 100644
--- a/js/binary/decoder.js
+++ b/js/binary/decoder.js
@@ -223,7 +223,7 @@
 jspb.BinaryDecoder = function(opt_bytes, opt_start, opt_length) {
   /**
    * Typed byte-wise view of the source buffer.
-   * @private {Uint8Array}
+   * @private {?Uint8Array}
    */
   this.bytes_ = null;
 
@@ -335,7 +335,7 @@
 
 /**
  * Returns the raw buffer.
- * @return {Uint8Array} The raw buffer.
+ * @return {?Uint8Array} The raw buffer.
  */
 jspb.BinaryDecoder.prototype.getBuffer = function() {
   return this.bytes_;
@@ -631,6 +631,7 @@
   return value.toString();
 };
 
+
 /**
  * Reads a 32-bit signed variant and returns its value as a string.
  *
@@ -950,14 +951,15 @@
  * Reads a block of raw bytes from the binary stream.
  *
  * @param {number} length The number of bytes to read.
- * @return {Uint8Array} The decoded block of bytes, or null if the length was
- *     invalid.
+ * @return {!Uint8Array} The decoded block of bytes, or an empty block if the
+ *     length was invalid.
  */
 jspb.BinaryDecoder.prototype.readBytes = function(length) {
   if (length < 0 ||
       this.cursor_ + length > this.bytes_.length) {
     this.error_ = true;
-    return null;
+    goog.asserts.fail('Invalid byte length!');
+    return new Uint8Array(0);
   }
 
   var result = this.bytes_.subarray(this.cursor_, this.cursor_ + length);
diff --git a/js/binary/decoder_test.js b/js/binary/decoder_test.js
index 27342e4..d045e91 100644
--- a/js/binary/decoder_test.js
+++ b/js/binary/decoder_test.js
@@ -44,11 +44,11 @@
 goog.require('goog.testing.asserts');
 goog.require('jspb.BinaryConstants');
 goog.require('jspb.BinaryDecoder');
-goog.require('jspb.BinaryWriter');
+goog.require('jspb.BinaryEncoder');
 
 
 /**
- * Tests raw encoding and decoding of unsigned types.
+ * Tests encoding and decoding of unsigned types.
  * @param {Function} readValue
  * @param {Function} writeValue
  * @param {number} epsilon
@@ -58,34 +58,38 @@
  */
 function doTestUnsignedValue(readValue,
     writeValue, epsilon, upperLimit, filter) {
-  var writer = new jspb.BinaryWriter();
+  var encoder = new jspb.BinaryEncoder();
 
   // Encode zero and limits.
-  writeValue.call(writer, filter(0));
-  writeValue.call(writer, filter(epsilon));
-  writeValue.call(writer, filter(upperLimit));
+  writeValue.call(encoder, filter(0));
+  writeValue.call(encoder, filter(epsilon));
+  writeValue.call(encoder, filter(upperLimit));
 
   // Encode positive values.
   for (var cursor = epsilon; cursor < upperLimit; cursor *= 1.1) {
-    writeValue.call(writer, filter(cursor));
+    writeValue.call(encoder, filter(cursor));
   }
 
-  var reader = jspb.BinaryDecoder.alloc(writer.getResultBuffer());
+  var decoder = jspb.BinaryDecoder.alloc(encoder.end());
 
   // Check zero and limits.
-  assertEquals(filter(0), readValue.call(reader));
-  assertEquals(filter(epsilon), readValue.call(reader));
-  assertEquals(filter(upperLimit), readValue.call(reader));
+  assertEquals(filter(0), readValue.call(decoder));
+  assertEquals(filter(epsilon), readValue.call(decoder));
+  assertEquals(filter(upperLimit), readValue.call(decoder));
 
   // Check positive values.
   for (var cursor = epsilon; cursor < upperLimit; cursor *= 1.1) {
-    if (filter(cursor) != readValue.call(reader)) throw 'fail!';
+    if (filter(cursor) != readValue.call(decoder)) throw 'fail!';
   }
+
+  // Encoding values outside the valid range should assert.
+  assertThrows(function() {writeValue.call(encoder, -1);});
+  assertThrows(function() {writeValue.call(encoder, upperLimit * 1.1);});
 }
 
 
 /**
- * Tests raw encoding and decoding of signed types.
+ * Tests encoding and decoding of signed types.
  * @param {Function} readValue
  * @param {Function} writeValue
  * @param {number} epsilon
@@ -96,44 +100,48 @@
  */
 function doTestSignedValue(readValue,
     writeValue, epsilon, lowerLimit, upperLimit, filter) {
-  var writer = new jspb.BinaryWriter();
+  var encoder = new jspb.BinaryEncoder();
 
   // Encode zero and limits.
-  writeValue.call(writer, filter(lowerLimit));
-  writeValue.call(writer, filter(-epsilon));
-  writeValue.call(writer, filter(0));
-  writeValue.call(writer, filter(epsilon));
-  writeValue.call(writer, filter(upperLimit));
+  writeValue.call(encoder, filter(lowerLimit));
+  writeValue.call(encoder, filter(-epsilon));
+  writeValue.call(encoder, filter(0));
+  writeValue.call(encoder, filter(epsilon));
+  writeValue.call(encoder, filter(upperLimit));
 
   var inputValues = [];
 
   // Encode negative values.
   for (var cursor = lowerLimit; cursor < -epsilon; cursor /= 1.1) {
     var val = filter(cursor);
-    writeValue.call(writer, val);
+    writeValue.call(encoder, val);
     inputValues.push(val);
   }
 
   // Encode positive values.
   for (var cursor = epsilon; cursor < upperLimit; cursor *= 1.1) {
     var val = filter(cursor);
-    writeValue.call(writer, val);
+    writeValue.call(encoder, val);
     inputValues.push(val);
   }
 
-  var reader = jspb.BinaryDecoder.alloc(writer.getResultBuffer());
+  var decoder = jspb.BinaryDecoder.alloc(encoder.end());
 
   // Check zero and limits.
-  assertEquals(filter(lowerLimit), readValue.call(reader));
-  assertEquals(filter(-epsilon), readValue.call(reader));
-  assertEquals(filter(0), readValue.call(reader));
-  assertEquals(filter(epsilon), readValue.call(reader));
-  assertEquals(filter(upperLimit), readValue.call(reader));
+  assertEquals(filter(lowerLimit), readValue.call(decoder));
+  assertEquals(filter(-epsilon), readValue.call(decoder));
+  assertEquals(filter(0), readValue.call(decoder));
+  assertEquals(filter(epsilon), readValue.call(decoder));
+  assertEquals(filter(upperLimit), readValue.call(decoder));
 
   // Verify decoded values.
   for (var i = 0; i < inputValues.length; i++) {
-    assertEquals(inputValues[i], readValue.call(reader));
+    assertEquals(inputValues[i], readValue.call(decoder));
   }
+
+  // Encoding values outside the valid range should assert.
+  assertThrows(function() {writeValue.call(encoder, lowerLimit * 1.1);});
+  assertThrows(function() {writeValue.call(encoder, upperLimit * 1.1);});
 }
 
 describe('binaryDecoderTest', function() {
@@ -169,7 +177,7 @@
    * Tests reading 64-bit integers as hash strings.
    */
   it('testHashStrings', function() {
-    var writer = new jspb.BinaryWriter();
+    var encoder = new jspb.BinaryEncoder();
 
     var hashA = String.fromCharCode(0x00, 0x00, 0x00, 0x00,
                                     0x00, 0x00, 0x00, 0x00);
@@ -180,17 +188,17 @@
     var hashD = String.fromCharCode(0xFF, 0xFF, 0xFF, 0xFF,
                                     0xFF, 0xFF, 0xFF, 0xFF);
 
-    writer.rawWriteVarintHash64(hashA);
-    writer.rawWriteVarintHash64(hashB);
-    writer.rawWriteVarintHash64(hashC);
-    writer.rawWriteVarintHash64(hashD);
+    encoder.writeVarintHash64(hashA);
+    encoder.writeVarintHash64(hashB);
+    encoder.writeVarintHash64(hashC);
+    encoder.writeVarintHash64(hashD);
 
-    writer.rawWriteFixedHash64(hashA);
-    writer.rawWriteFixedHash64(hashB);
-    writer.rawWriteFixedHash64(hashC);
-    writer.rawWriteFixedHash64(hashD);
+    encoder.writeFixedHash64(hashA);
+    encoder.writeFixedHash64(hashB);
+    encoder.writeFixedHash64(hashC);
+    encoder.writeFixedHash64(hashD);
 
-    var decoder = jspb.BinaryDecoder.alloc(writer.getResultBuffer());
+    var decoder = jspb.BinaryDecoder.alloc(encoder.end());
 
     assertEquals(hashA, decoder.readVarintHash64());
     assertEquals(hashB, decoder.readVarintHash64());
@@ -214,8 +222,8 @@
     assertThrows(function() {decoder.readUint64()});
 
     // Overlong varints should trigger assertions.
-    decoder.setBlock(
-        [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0]);
+    decoder.setBlock([255, 255, 255, 255, 255, 255,
+                      255, 255, 255, 255, 255, 0]);
     assertThrows(function() {decoder.readUnsignedVarint64()});
     decoder.reset();
     assertThrows(function() {decoder.readSignedVarint64()});
@@ -244,61 +252,61 @@
 
 
   /**
-   * Tests raw encoding and decoding of unsigned integers.
+   * Tests encoding and decoding of unsigned integers.
    */
-  it('testRawUnsigned', function() {
+  it('testUnsignedIntegers', function() {
     doTestUnsignedValue(
         jspb.BinaryDecoder.prototype.readUint8,
-        jspb.BinaryWriter.prototype.rawWriteUint8,
+        jspb.BinaryEncoder.prototype.writeUint8,
         1, 0xFF, Math.round);
 
     doTestUnsignedValue(
         jspb.BinaryDecoder.prototype.readUint16,
-        jspb.BinaryWriter.prototype.rawWriteUint16,
+        jspb.BinaryEncoder.prototype.writeUint16,
         1, 0xFFFF, Math.round);
 
     doTestUnsignedValue(
         jspb.BinaryDecoder.prototype.readUint32,
-        jspb.BinaryWriter.prototype.rawWriteUint32,
+        jspb.BinaryEncoder.prototype.writeUint32,
         1, 0xFFFFFFFF, Math.round);
 
     doTestUnsignedValue(
         jspb.BinaryDecoder.prototype.readUint64,
-        jspb.BinaryWriter.prototype.rawWriteUint64,
+        jspb.BinaryEncoder.prototype.writeUint64,
         1, Math.pow(2, 64) - 1025, Math.round);
   });
 
 
   /**
-   * Tests raw encoding and decoding of signed integers.
+   * Tests encoding and decoding of signed integers.
    */
-  it('testRawSigned', function() {
+  it('testSignedIntegers', function() {
     doTestSignedValue(
         jspb.BinaryDecoder.prototype.readInt8,
-        jspb.BinaryWriter.prototype.rawWriteInt8,
+        jspb.BinaryEncoder.prototype.writeInt8,
         1, -0x80, 0x7F, Math.round);
 
     doTestSignedValue(
         jspb.BinaryDecoder.prototype.readInt16,
-        jspb.BinaryWriter.prototype.rawWriteInt16,
+        jspb.BinaryEncoder.prototype.writeInt16,
         1, -0x8000, 0x7FFF, Math.round);
 
     doTestSignedValue(
         jspb.BinaryDecoder.prototype.readInt32,
-        jspb.BinaryWriter.prototype.rawWriteInt32,
+        jspb.BinaryEncoder.prototype.writeInt32,
         1, -0x80000000, 0x7FFFFFFF, Math.round);
 
     doTestSignedValue(
         jspb.BinaryDecoder.prototype.readInt64,
-        jspb.BinaryWriter.prototype.rawWriteInt64,
+        jspb.BinaryEncoder.prototype.writeInt64,
         1, -Math.pow(2, 63), Math.pow(2, 63) - 513, Math.round);
   });
 
 
   /**
-   * Tests raw encoding and decoding of floats.
+   * Tests encoding and decoding of floats.
    */
-  it('testRawFloats', function() {
+  it('testFloats', function() {
     /**
      * @param {number} x
      * @return {number}
@@ -310,7 +318,7 @@
     }
     doTestSignedValue(
         jspb.BinaryDecoder.prototype.readFloat,
-        jspb.BinaryWriter.prototype.rawWriteFloat,
+        jspb.BinaryEncoder.prototype.writeFloat,
         jspb.BinaryConstants.FLOAT32_EPS,
         -jspb.BinaryConstants.FLOAT32_MAX,
         jspb.BinaryConstants.FLOAT32_MAX,
@@ -318,7 +326,7 @@
 
     doTestSignedValue(
         jspb.BinaryDecoder.prototype.readDouble,
-        jspb.BinaryWriter.prototype.rawWriteDouble,
+        jspb.BinaryEncoder.prototype.writeDouble,
         jspb.BinaryConstants.FLOAT64_EPS * 10,
         -jspb.BinaryConstants.FLOAT64_MAX,
         jspb.BinaryConstants.FLOAT64_MAX,
diff --git a/js/binary/encoder.js b/js/binary/encoder.js
new file mode 100644
index 0000000..c9b0c2a
--- /dev/null
+++ b/js/binary/encoder.js
@@ -0,0 +1,430 @@
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/**
+ * @fileoverview BinaryEncode defines methods for encoding Javascript values
+ * into arrays of bytes compatible with the Protocol Buffer wire format.
+ *
+ * @author aappleby@google.com (Austin Appleby)
+ */
+
+goog.provide('jspb.BinaryEncoder');
+
+goog.require('goog.asserts');
+goog.require('jspb.BinaryConstants');
+goog.require('jspb.utils');
+
+
+
+/**
+ * BinaryEncoder implements encoders for all the wire types specified in
+ * https://developers.google.com/protocol-buffers/docs/encoding.
+ *
+ * @constructor
+ * @struct
+ */
+jspb.BinaryEncoder = function() {
+  /** @private {!Array.<number>} */
+  this.buffer_ = [];
+};
+
+
+/**
+ * @return {number}
+ */
+jspb.BinaryEncoder.prototype.length = function() {
+  return this.buffer_.length;
+};
+
+
+/**
+ * @return {!Array.<number>}
+ */
+jspb.BinaryEncoder.prototype.end = function() {
+  var buffer = this.buffer_;
+  this.buffer_ = [];
+  return buffer;
+};
+
+
+/**
+ * Encodes a 64-bit integer in 32:32 split representation into its wire-format
+ * varint representation and stores it in the buffer.
+ * @param {number} lowBits The low 32 bits of the int.
+ * @param {number} highBits The high 32 bits of the int.
+ */
+jspb.BinaryEncoder.prototype.writeSplitVarint64 = function(lowBits, highBits) {
+  goog.asserts.assert(lowBits == Math.floor(lowBits));
+  goog.asserts.assert(highBits == Math.floor(highBits));
+  goog.asserts.assert((lowBits >= 0) &&
+                      (lowBits < jspb.BinaryConstants.TWO_TO_32));
+  goog.asserts.assert((highBits >= 0) &&
+                      (highBits < jspb.BinaryConstants.TWO_TO_32));
+
+  // Break the binary representation into chunks of 7 bits, set the 8th bit
+  // in each chunk if it's not the final chunk, and append to the result.
+  while (highBits > 0 || lowBits > 127) {
+    this.buffer_.push((lowBits & 0x7f) | 0x80);
+    lowBits = ((lowBits >>> 7) | (highBits << 25)) >>> 0;
+    highBits = highBits >>> 7;
+  }
+  this.buffer_.push(lowBits);
+};
+
+
+/**
+ * Encodes a 32-bit unsigned integer into its wire-format varint representation
+ * and stores it in the buffer.
+ * @param {number} value The integer to convert.
+ */
+jspb.BinaryEncoder.prototype.writeUnsignedVarint32 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= 0) &&
+                      (value < jspb.BinaryConstants.TWO_TO_32));
+
+  while (value > 127) {
+    this.buffer_.push((value & 0x7f) | 0x80);
+    value = value >>> 7;
+  }
+
+  this.buffer_.push(value);
+};
+
+
+/**
+ * Encodes a 32-bit signed integer into its wire-format varint representation
+ * and stores it in the buffer.
+ * @param {number} value The integer to convert.
+ */
+jspb.BinaryEncoder.prototype.writeSignedVarint32 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_31) &&
+                      (value < jspb.BinaryConstants.TWO_TO_31));
+
+  // Use the unsigned version if the value is not negative.
+  if (value >= 0) {
+    this.writeUnsignedVarint32(value);
+    return;
+  }
+
+  // Write nine bytes with a _signed_ right shift so we preserve the sign bit.
+  for (var i = 0; i < 9; i++) {
+    this.buffer_.push((value & 0x7f) | 0x80);
+    value = value >> 7;
+  }
+
+  // The above loop writes out 63 bits, so the last byte is always the sign bit
+  // which is always set for negative numbers.
+  this.buffer_.push(1);
+};
+
+
+/**
+ * Encodes a 64-bit unsigned integer into its wire-format varint representation
+ * and stores it in the buffer. Integers that are not representable in 64 bits
+ * will be truncated.
+ * @param {number} value The integer to convert.
+ */
+jspb.BinaryEncoder.prototype.writeUnsignedVarint64 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= 0) &&
+                      (value < jspb.BinaryConstants.TWO_TO_64));
+  jspb.utils.splitInt64(value);
+  this.writeSplitVarint64(jspb.utils.split64Low,
+                          jspb.utils.split64High);
+};
+
+
+/**
+ * Encodes a 64-bit signed integer into its wire-format varint representation
+ * and stores it in the buffer. Integers that are not representable in 64 bits
+ * will be truncated.
+ * @param {number} value The integer to convert.
+ */
+jspb.BinaryEncoder.prototype.writeSignedVarint64 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_63) &&
+                      (value < jspb.BinaryConstants.TWO_TO_63));
+  jspb.utils.splitInt64(value);
+  this.writeSplitVarint64(jspb.utils.split64Low,
+                          jspb.utils.split64High);
+};
+
+
+/**
+ * Encodes a JavaScript integer into its wire-format, zigzag-encoded varint
+ * representation and stores it in the buffer.
+ * @param {number} value The integer to convert.
+ */
+jspb.BinaryEncoder.prototype.writeZigzagVarint32 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_31) &&
+                      (value < jspb.BinaryConstants.TWO_TO_31));
+  this.writeUnsignedVarint32(((value << 1) ^ (value >> 31)) >>> 0);
+};
+
+
+/**
+ * Encodes a JavaScript integer into its wire-format, zigzag-encoded varint
+ * representation and stores it in the buffer. Integers not representable in 64
+ * bits will be truncated.
+ * @param {number} value The integer to convert.
+ */
+jspb.BinaryEncoder.prototype.writeZigzagVarint64 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_63) &&
+                      (value < jspb.BinaryConstants.TWO_TO_63));
+  jspb.utils.splitZigzag64(value);
+  this.writeSplitVarint64(jspb.utils.split64Low,
+                          jspb.utils.split64High);
+};
+
+
+/**
+ * Writes a 8-bit unsigned integer to the buffer. Numbers outside the range
+ * [0,2^8) will be truncated.
+ * @param {number} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeUint8 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= 0) && (value < 256));
+  this.buffer_.push((value >>> 0) & 0xFF);
+};
+
+
+/**
+ * Writes a 16-bit unsigned integer to the buffer. Numbers outside the
+ * range [0,2^16) will be truncated.
+ * @param {number} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeUint16 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= 0) && (value < 65536));
+  this.buffer_.push((value >>> 0) & 0xFF);
+  this.buffer_.push((value >>> 8) & 0xFF);
+};
+
+
+/**
+ * Writes a 32-bit unsigned integer to the buffer. Numbers outside the
+ * range [0,2^32) will be truncated.
+ * @param {number} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeUint32 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= 0) &&
+                      (value < jspb.BinaryConstants.TWO_TO_32));
+  this.buffer_.push((value >>> 0) & 0xFF);
+  this.buffer_.push((value >>> 8) & 0xFF);
+  this.buffer_.push((value >>> 16) & 0xFF);
+  this.buffer_.push((value >>> 24) & 0xFF);
+};
+
+
+/**
+ * Writes a 64-bit unsigned integer to the buffer. Numbers outside the
+ * range [0,2^64) will be truncated.
+ * @param {number} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeUint64 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= 0) &&
+                      (value < jspb.BinaryConstants.TWO_TO_64));
+  jspb.utils.splitUint64(value);
+  this.writeUint32(jspb.utils.split64Low);
+  this.writeUint32(jspb.utils.split64High);
+};
+
+
+/**
+ * Writes a 8-bit integer to the buffer. Numbers outside the range
+ * [-2^7,2^7) will be truncated.
+ * @param {number} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeInt8 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= -128) && (value < 128));
+  this.buffer_.push((value >>> 0) & 0xFF);
+};
+
+
+/**
+ * Writes a 16-bit integer to the buffer. Numbers outside the range
+ * [-2^15,2^15) will be truncated.
+ * @param {number} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeInt16 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= -32768) && (value < 32768));
+  this.buffer_.push((value >>> 0) & 0xFF);
+  this.buffer_.push((value >>> 8) & 0xFF);
+};
+
+
+/**
+ * Writes a 32-bit integer to the buffer. Numbers outside the range
+ * [-2^31,2^31) will be truncated.
+ * @param {number} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeInt32 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_31) &&
+                      (value < jspb.BinaryConstants.TWO_TO_31));
+  this.buffer_.push((value >>> 0) & 0xFF);
+  this.buffer_.push((value >>> 8) & 0xFF);
+  this.buffer_.push((value >>> 16) & 0xFF);
+  this.buffer_.push((value >>> 24) & 0xFF);
+};
+
+
+/**
+ * Writes a 64-bit integer to the buffer. Numbers outside the range
+ * [-2^63,2^63) will be truncated.
+ * @param {number} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeInt64 = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_63) &&
+                      (value < jspb.BinaryConstants.TWO_TO_63));
+  jspb.utils.splitInt64(value);
+  this.writeUint32(jspb.utils.split64Low);
+  this.writeUint32(jspb.utils.split64High);
+};
+
+
+/**
+ * Writes a single-precision floating point value to the buffer. Numbers
+ * requiring more than 32 bits of precision will be truncated.
+ * @param {number} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeFloat = function(value) {
+  goog.asserts.assert((value >= -jspb.BinaryConstants.FLOAT32_MAX) &&
+                      (value <= jspb.BinaryConstants.FLOAT32_MAX));
+  jspb.utils.splitFloat32(value);
+  this.writeUint32(jspb.utils.split64Low);
+};
+
+
+/**
+ * Writes a double-precision floating point value to the buffer. As this is
+ * the native format used by JavaScript, no precision will be lost.
+ * @param {number} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeDouble = function(value) {
+  goog.asserts.assert((value >= -jspb.BinaryConstants.FLOAT64_MAX) &&
+                      (value <= jspb.BinaryConstants.FLOAT64_MAX));
+  jspb.utils.splitFloat64(value);
+  this.writeUint32(jspb.utils.split64Low);
+  this.writeUint32(jspb.utils.split64High);
+};
+
+
+/**
+ * Writes a boolean value to the buffer as a varint.
+ * @param {boolean} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeBool = function(value) {
+  goog.asserts.assert(goog.isBoolean(value));
+  this.buffer_.push(value ? 1 : 0);
+};
+
+
+/**
+ * Writes an enum value to the buffer as a varint.
+ * @param {number} value The value to write.
+ */
+jspb.BinaryEncoder.prototype.writeEnum = function(value) {
+  goog.asserts.assert(value == Math.floor(value));
+  goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_31) &&
+                      (value < jspb.BinaryConstants.TWO_TO_31));
+  this.writeSignedVarint32(value);
+};
+
+
+/**
+ * Writes an arbitrary byte array to the buffer.
+ * @param {!Uint8Array} bytes The array of bytes to write.
+ */
+jspb.BinaryEncoder.prototype.writeBytes = function(bytes) {
+  this.buffer_.push.apply(this.buffer_, bytes);
+};
+
+
+/**
+ * Writes a 64-bit hash string (8 characters @ 8 bits of data each) to the
+ * buffer as a varint.
+ * @param {string} hash The hash to write.
+ */
+jspb.BinaryEncoder.prototype.writeVarintHash64 = function(hash) {
+  jspb.utils.splitHash64(hash);
+  this.writeSplitVarint64(jspb.utils.split64Low,
+                          jspb.utils.split64High);
+};
+
+
+/**
+ * Writes a 64-bit hash string (8 characters @ 8 bits of data each) to the
+ * buffer as a fixed64.
+ * @param {string} hash The hash to write.
+ */
+jspb.BinaryEncoder.prototype.writeFixedHash64 = function(hash) {
+  jspb.utils.splitHash64(hash);
+  this.writeUint32(jspb.utils.split64Low);
+  this.writeUint32(jspb.utils.split64High);
+};
+
+
+/**
+ * Writes a UTF16 Javascript string to the buffer encoded as UTF8.
+ * TODO(aappleby): Add support for surrogate pairs, reject unpaired surrogates.
+ * @param {string} value The string to write.
+ * @return {number} The number of bytes used to encode the string.
+ */
+jspb.BinaryEncoder.prototype.writeString = function(value) {
+  var oldLength = this.buffer_.length;
+
+  // UTF16 to UTF8 conversion loop swiped from goog.crypt.stringToUtf8ByteArray.
+  for (var i = 0; i < value.length; i++) {
+    var c = value.charCodeAt(i);
+    if (c < 128) {
+      this.buffer_.push(c);
+    } else if (c < 2048) {
+      this.buffer_.push((c >> 6) | 192);
+      this.buffer_.push((c & 63) | 128);
+    } else {
+      this.buffer_.push((c >> 12) | 224);
+      this.buffer_.push(((c >> 6) & 63) | 128);
+      this.buffer_.push((c & 63) | 128);
+    }
+  }
+
+  var length = this.buffer_.length - oldLength;
+  return length;
+};
diff --git a/js/binary/proto_test.js b/js/binary/proto_test.js
index 3b4aa17..14d0f42 100644
--- a/js/binary/proto_test.js
+++ b/js/binary/proto_test.js
@@ -30,7 +30,9 @@
 
 // Test suite is written using Jasmine -- see http://jasmine.github.io/
 
+goog.require('goog.crypt.base64');
 goog.require('goog.testing.asserts');
+goog.require('jspb.Message');
 
 // CommonJS-LoadFromFile: ../testbinary_pb proto.jspb.test
 goog.require('proto.jspb.test.ExtendsWithMessage');
@@ -38,9 +40,61 @@
 goog.require('proto.jspb.test.ForeignMessage');
 goog.require('proto.jspb.test.TestAllTypes');
 goog.require('proto.jspb.test.TestExtendable');
+goog.require('proto.jspb.test.extendOptionalBool');
+goog.require('proto.jspb.test.extendOptionalBytes');
+goog.require('proto.jspb.test.extendOptionalDouble');
+goog.require('proto.jspb.test.extendOptionalFixed32');
+goog.require('proto.jspb.test.extendOptionalFixed64');
+goog.require('proto.jspb.test.extendOptionalFloat');
+goog.require('proto.jspb.test.extendOptionalForeignEnum');
+goog.require('proto.jspb.test.extendOptionalInt32');
+goog.require('proto.jspb.test.extendOptionalInt64');
+goog.require('proto.jspb.test.extendOptionalSfixed32');
+goog.require('proto.jspb.test.extendOptionalSfixed64');
+goog.require('proto.jspb.test.extendOptionalSint32');
+goog.require('proto.jspb.test.extendOptionalSint64');
+goog.require('proto.jspb.test.extendOptionalString');
+goog.require('proto.jspb.test.extendOptionalUint32');
+goog.require('proto.jspb.test.extendOptionalUint64');
+goog.require('proto.jspb.test.extendPackedRepeatedBoolList');
+goog.require('proto.jspb.test.extendPackedRepeatedDoubleList');
+goog.require('proto.jspb.test.extendPackedRepeatedFixed32List');
+goog.require('proto.jspb.test.extendPackedRepeatedFixed64List');
+goog.require('proto.jspb.test.extendPackedRepeatedFloatList');
+goog.require('proto.jspb.test.extendPackedRepeatedForeignEnumList');
+goog.require('proto.jspb.test.extendPackedRepeatedInt32List');
+goog.require('proto.jspb.test.extendPackedRepeatedInt64List');
+goog.require('proto.jspb.test.extendPackedRepeatedSfixed32List');
+goog.require('proto.jspb.test.extendPackedRepeatedSfixed64List');
+goog.require('proto.jspb.test.extendPackedRepeatedSint32List');
+goog.require('proto.jspb.test.extendPackedRepeatedSint64List');
+goog.require('proto.jspb.test.extendPackedRepeatedUint32List');
+goog.require('proto.jspb.test.extendPackedRepeatedUint64List');
+goog.require('proto.jspb.test.extendRepeatedBoolList');
+goog.require('proto.jspb.test.extendRepeatedBytesList');
+goog.require('proto.jspb.test.extendRepeatedDoubleList');
+goog.require('proto.jspb.test.extendRepeatedFixed32List');
+goog.require('proto.jspb.test.extendRepeatedFixed64List');
+goog.require('proto.jspb.test.extendRepeatedFloatList');
+goog.require('proto.jspb.test.extendRepeatedForeignEnumList');
+goog.require('proto.jspb.test.extendRepeatedInt32List');
+goog.require('proto.jspb.test.extendRepeatedInt64List');
+goog.require('proto.jspb.test.extendRepeatedSfixed32List');
+goog.require('proto.jspb.test.extendRepeatedSfixed64List');
+goog.require('proto.jspb.test.extendRepeatedSint32List');
+goog.require('proto.jspb.test.extendRepeatedSint64List');
+goog.require('proto.jspb.test.extendRepeatedStringList');
+goog.require('proto.jspb.test.extendRepeatedUint32List');
+goog.require('proto.jspb.test.extendRepeatedUint64List');
+
 
 var suite = {};
 
+var BYTES = new Uint8Array([1, 2, 8, 9]);
+
+var BYTES_B64 = goog.crypt.base64.encodeByteArray(BYTES);
+
+
 /**
  * Helper: fill all fields on a TestAllTypes message.
  * @param {proto.jspb.test.TestAllTypes} msg
@@ -62,7 +116,7 @@
   msg.setOptionalDouble(-1.5);
   msg.setOptionalBool(true);
   msg.setOptionalString('hello world');
-  msg.setOptionalBytes('bytes');
+  msg.setOptionalBytes(BYTES);
   msg.setOptionalGroup(new proto.jspb.test.TestAllTypes.OptionalGroup());
   msg.getOptionalGroup().setA(100);
   var submsg = new proto.jspb.test.ForeignMessage();
@@ -71,6 +125,7 @@
   msg.setOptionalForeignEnum(proto.jspb.test.ForeignEnum.FOREIGN_FOO);
   msg.setOneofString('oneof');
 
+
   msg.setRepeatedInt32List([-42]);
   msg.setRepeatedInt64List([-0x7fffffff00000000]);
   msg.setRepeatedUint32List([0x80000000]);
@@ -85,7 +140,7 @@
   msg.setRepeatedDoubleList([-1.5]);
   msg.setRepeatedBoolList([true]);
   msg.setRepeatedStringList(['hello world']);
-  msg.setRepeatedBytesList(['bytes']);
+  msg.setRepeatedBytesList([BYTES, BYTES]);
   msg.setRepeatedGroupList([new proto.jspb.test.TestAllTypes.RepeatedGroup()]);
   msg.getRepeatedGroupList()[0].setA(100);
   submsg = new proto.jspb.test.ForeignMessage();
@@ -106,106 +161,115 @@
   msg.setPackedRepeatedFloatList([1.5]);
   msg.setPackedRepeatedDoubleList([-1.5]);
   msg.setPackedRepeatedBoolList([true]);
+
 }
 
 
 /**
- * Helper: compare a bytes field to a string with codepoints 0--255.
+ * Helper: compare a bytes field to an expected value
  * @param {Uint8Array|string} arr
- * @param {string} str
+ * @param {Uint8Array} expected
  * @return {boolean}
  */
-function bytesCompare(arr, str) {
-  if (arr.length != str.length) {
+function bytesCompare(arr, expected) {
+  if (goog.isString(arr)) {
+    arr = goog.crypt.base64.decodeStringToUint8Array(arr);
+  }
+  if (arr.length != expected.length) {
     return false;
   }
-  if (typeof arr == 'string') {
-    for (var i = 0; i < arr.length; i++) {
-      if (arr.charCodeAt(i) != str.charCodeAt(i)) {
-        return false;
-      }
+  for (var i = 0; i < arr.length; i++) {
+    if (arr[i] != expected[i]) {
+      return false;
     }
-    return true;
-  } else {
-    for (var i = 0; i < arr.length; i++) {
-      if (arr[i] != str.charCodeAt(i)) {
-        return false;
-      }
-    }
-    return true;
   }
+  return true;
 }
 
 
 /**
  * Helper: verify contents of given TestAllTypes message as set by
  * fillAllFields().
- * @param {proto.jspb.test.TestAllTypes} msg
+ * @param {proto.jspb.test.TestAllTypes} original
+ * @param {proto.jspb.test.TestAllTypes} copy
  */
-function checkAllFields(msg) {
-  assertEquals(msg.getOptionalInt32(), -42);
-  assertEquals(msg.getOptionalInt64(), -0x7fffffff00000000);
-  assertEquals(msg.getOptionalUint32(), 0x80000000);
-  assertEquals(msg.getOptionalUint64(), 0xf000000000000000);
-  assertEquals(msg.getOptionalSint32(), -100);
-  assertEquals(msg.getOptionalSint64(), -0x8000000000000000);
-  assertEquals(msg.getOptionalFixed32(), 1234);
-  assertEquals(msg.getOptionalFixed64(), 0x1234567800000000);
-  assertEquals(msg.getOptionalSfixed32(), -1234);
-  assertEquals(msg.getOptionalSfixed64(), -0x1234567800000000);
-  assertEquals(msg.getOptionalFloat(), 1.5);
-  assertEquals(msg.getOptionalDouble(), -1.5);
-  assertEquals(msg.getOptionalBool(), true);
-  assertEquals(msg.getOptionalString(), 'hello world');
-  assertEquals(true, bytesCompare(msg.getOptionalBytes(), 'bytes'));
-  assertEquals(msg.getOptionalGroup().getA(), 100);
-  assertEquals(msg.getOptionalForeignMessage().getC(), 16);
-  assertEquals(msg.getOptionalForeignEnum(),
+function checkAllFields(original, copy) {
+  assertTrue(jspb.Message.equals(original, copy));
+
+  assertEquals(copy.getOptionalInt32(), -42);
+  assertEquals(copy.getOptionalInt64(), -0x7fffffff00000000);
+  assertEquals(copy.getOptionalUint32(), 0x80000000);
+  assertEquals(copy.getOptionalUint64(), 0xf000000000000000);
+  assertEquals(copy.getOptionalSint32(), -100);
+  assertEquals(copy.getOptionalSint64(), -0x8000000000000000);
+  assertEquals(copy.getOptionalFixed32(), 1234);
+  assertEquals(copy.getOptionalFixed64(), 0x1234567800000000);
+  assertEquals(copy.getOptionalSfixed32(), -1234);
+  assertEquals(copy.getOptionalSfixed64(), -0x1234567800000000);
+  assertEquals(copy.getOptionalFloat(), 1.5);
+  assertEquals(copy.getOptionalDouble(), -1.5);
+  assertEquals(copy.getOptionalBool(), true);
+  assertEquals(copy.getOptionalString(), 'hello world');
+  assertEquals(true, bytesCompare(copy.getOptionalBytes(), BYTES));
+  assertEquals(true, bytesCompare(copy.getOptionalBytes_asU8(), BYTES));
+  assertEquals(
+      copy.getOptionalBytes_asB64(), goog.crypt.base64.encodeByteArray(BYTES));
+
+  assertEquals(copy.getOptionalGroup().getA(), 100);
+  assertEquals(copy.getOptionalForeignMessage().getC(), 16);
+  assertEquals(copy.getOptionalForeignEnum(),
       proto.jspb.test.ForeignEnum.FOREIGN_FOO);
-  assertEquals(msg.getOneofString(), 'oneof');
-  assertEquals(msg.getOneofFieldCase(),
+
+
+  assertEquals(copy.getOneofString(), 'oneof');
+  assertEquals(copy.getOneofFieldCase(),
       proto.jspb.test.TestAllTypes.OneofFieldCase.ONEOF_STRING);
 
-  assertElementsEquals(msg.getRepeatedInt32List(), [-42]);
-  assertElementsEquals(msg.getRepeatedInt64List(), [-0x7fffffff00000000]);
-  assertElementsEquals(msg.getRepeatedUint32List(), [0x80000000]);
-  assertElementsEquals(msg.getRepeatedUint64List(), [0xf000000000000000]);
-  assertElementsEquals(msg.getRepeatedSint32List(), [-100]);
-  assertElementsEquals(msg.getRepeatedSint64List(), [-0x8000000000000000]);
-  assertElementsEquals(msg.getRepeatedFixed32List(), [1234]);
-  assertElementsEquals(msg.getRepeatedFixed64List(), [0x1234567800000000]);
-  assertElementsEquals(msg.getRepeatedSfixed32List(), [-1234]);
-  assertElementsEquals(msg.getRepeatedSfixed64List(), [-0x1234567800000000]);
-  assertElementsEquals(msg.getRepeatedFloatList(), [1.5]);
-  assertElementsEquals(msg.getRepeatedDoubleList(), [-1.5]);
-  assertElementsEquals(msg.getRepeatedBoolList(), [true]);
-  assertElementsEquals(msg.getRepeatedStringList(), ['hello world']);
-  assertEquals(msg.getRepeatedBytesList().length, 1);
-  assertEquals(true, bytesCompare(msg.getRepeatedBytesList()[0], 'bytes'));
-  assertEquals(msg.getRepeatedGroupList().length, 1);
-  assertEquals(msg.getRepeatedGroupList()[0].getA(), 100);
-  assertEquals(msg.getRepeatedForeignMessageList().length, 1);
-  assertEquals(msg.getRepeatedForeignMessageList()[0].getC(), 1000);
-  assertElementsEquals(msg.getRepeatedForeignEnumList(),
+  assertElementsEquals(copy.getRepeatedInt32List(), [-42]);
+  assertElementsEquals(copy.getRepeatedInt64List(), [-0x7fffffff00000000]);
+  assertElementsEquals(copy.getRepeatedUint32List(), [0x80000000]);
+  assertElementsEquals(copy.getRepeatedUint64List(), [0xf000000000000000]);
+  assertElementsEquals(copy.getRepeatedSint32List(), [-100]);
+  assertElementsEquals(copy.getRepeatedSint64List(), [-0x8000000000000000]);
+  assertElementsEquals(copy.getRepeatedFixed32List(), [1234]);
+  assertElementsEquals(copy.getRepeatedFixed64List(), [0x1234567800000000]);
+  assertElementsEquals(copy.getRepeatedSfixed32List(), [-1234]);
+  assertElementsEquals(copy.getRepeatedSfixed64List(), [-0x1234567800000000]);
+  assertElementsEquals(copy.getRepeatedFloatList(), [1.5]);
+  assertElementsEquals(copy.getRepeatedDoubleList(), [-1.5]);
+  assertElementsEquals(copy.getRepeatedBoolList(), [true]);
+  assertElementsEquals(copy.getRepeatedStringList(), ['hello world']);
+  assertEquals(copy.getRepeatedBytesList().length, 2);
+  assertEquals(true, bytesCompare(copy.getRepeatedBytesList_asU8()[0], BYTES));
+  assertEquals(true, bytesCompare(copy.getRepeatedBytesList()[0], BYTES));
+  assertEquals(true, bytesCompare(copy.getRepeatedBytesList_asU8()[1], BYTES));
+  assertEquals(copy.getRepeatedBytesList_asB64()[0], BYTES_B64);
+  assertEquals(copy.getRepeatedBytesList_asB64()[1], BYTES_B64);
+  assertEquals(copy.getRepeatedGroupList().length, 1);
+  assertEquals(copy.getRepeatedGroupList()[0].getA(), 100);
+  assertEquals(copy.getRepeatedForeignMessageList().length, 1);
+  assertEquals(copy.getRepeatedForeignMessageList()[0].getC(), 1000);
+  assertElementsEquals(copy.getRepeatedForeignEnumList(),
       [proto.jspb.test.ForeignEnum.FOREIGN_FOO]);
 
-  assertElementsEquals(msg.getPackedRepeatedInt32List(), [-42]);
-  assertElementsEquals(msg.getPackedRepeatedInt64List(),
+  assertElementsEquals(copy.getPackedRepeatedInt32List(), [-42]);
+  assertElementsEquals(copy.getPackedRepeatedInt64List(),
       [-0x7fffffff00000000]);
-  assertElementsEquals(msg.getPackedRepeatedUint32List(), [0x80000000]);
-  assertElementsEquals(msg.getPackedRepeatedUint64List(), [0xf000000000000000]);
-  assertElementsEquals(msg.getPackedRepeatedSint32List(), [-100]);
-  assertElementsEquals(msg.getPackedRepeatedSint64List(),
+  assertElementsEquals(copy.getPackedRepeatedUint32List(), [0x80000000]);
+  assertElementsEquals(copy.getPackedRepeatedUint64List(),
+      [0xf000000000000000]);
+  assertElementsEquals(copy.getPackedRepeatedSint32List(), [-100]);
+  assertElementsEquals(copy.getPackedRepeatedSint64List(),
       [-0x8000000000000000]);
-  assertElementsEquals(msg.getPackedRepeatedFixed32List(), [1234]);
-  assertElementsEquals(msg.getPackedRepeatedFixed64List(),
+  assertElementsEquals(copy.getPackedRepeatedFixed32List(), [1234]);
+  assertElementsEquals(copy.getPackedRepeatedFixed64List(),
       [0x1234567800000000]);
-  assertElementsEquals(msg.getPackedRepeatedSfixed32List(), [-1234]);
-  assertElementsEquals(msg.getPackedRepeatedSfixed64List(),
+  assertElementsEquals(copy.getPackedRepeatedSfixed32List(), [-1234]);
+  assertElementsEquals(copy.getPackedRepeatedSfixed64List(),
       [-0x1234567800000000]);
-  assertElementsEquals(msg.getPackedRepeatedFloatList(), [1.5]);
-  assertElementsEquals(msg.getPackedRepeatedDoubleList(), [-1.5]);
-  assertElementsEquals(msg.getPackedRepeatedBoolList(), [true]);
+  assertElementsEquals(copy.getPackedRepeatedFloatList(), [1.5]);
+  assertElementsEquals(copy.getPackedRepeatedDoubleList(), [-1.5]);
+
 }
 
 
@@ -242,14 +306,13 @@
       msg.getExtension(proto.jspb.test.extendOptionalBool));
   assertEquals('hello world',
       msg.getExtension(proto.jspb.test.extendOptionalString));
-  assertEquals(true,
-      bytesCompare(msg.getExtension(proto.jspb.test.extendOptionalBytes),
-        'bytes'));
+  assertEquals(
+      true, bytesCompare(
+                msg.getExtension(proto.jspb.test.extendOptionalBytes), BYTES));
   assertEquals(16,
       msg.getExtension(
           proto.jspb.test.ExtendsWithMessage.optionalExtension).getFoo());
-  assertEquals(proto.jspb.test.ForeignEnum.FOREIGN_FOO,
-      msg.getExtension(proto.jspb.test.extendOptionalForeignEnum));
+
 
   assertElementsEquals(
       msg.getExtension(proto.jspb.test.extendRepeatedInt32List),
@@ -293,10 +356,10 @@
   assertElementsEquals(
       msg.getExtension(proto.jspb.test.extendRepeatedStringList),
       ['hello world']);
-  assertEquals(true,
+  assertEquals(
+      true,
       bytesCompare(
-          msg.getExtension(proto.jspb.test.extendRepeatedBytesList)[0],
-          'bytes'));
+          msg.getExtension(proto.jspb.test.extendRepeatedBytesList)[0], BYTES));
   assertEquals(1000,
       msg.getExtension(
           proto.jspb.test.ExtendsWithMessage.repeatedExtensionList)[0]
@@ -305,6 +368,7 @@
       msg.getExtension(proto.jspb.test.extendRepeatedForeignEnumList),
       [proto.jspb.test.ForeignEnum.FOREIGN_FOO]);
 
+
   assertElementsEquals(
       msg.getExtension(proto.jspb.test.extendPackedRepeatedInt32List),
       [-42]);
@@ -347,6 +411,7 @@
   assertElementsEquals(
       msg.getExtension(proto.jspb.test.extendPackedRepeatedForeignEnumList),
       [proto.jspb.test.ForeignEnum.FOREIGN_FOO]);
+
 }
 
 
@@ -360,9 +425,82 @@
     fillAllFields(msg);
     var encoded = msg.serializeBinary();
     var decoded = proto.jspb.test.TestAllTypes.deserializeBinary(encoded);
-    checkAllFields(decoded);
+    checkAllFields(msg, decoded);
   });
 
+  /**
+   * Test that base64 string and Uint8Array are interchangeable in bytes fields.
+   */
+  it('testBytesFieldsGettersInterop', function() {
+    var msg = new proto.jspb.test.TestAllTypes();
+    // Set from a base64 string and check all the getters work.
+    msg.setOptionalBytes(BYTES_B64);
+    assertTrue(bytesCompare(msg.getOptionalBytes_asU8(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes_asB64(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes(), BYTES));
+
+    // Test binary serialize round trip doesn't break it.
+    msg = proto.jspb.test.TestAllTypes.deserializeBinary(msg.serializeBinary());
+    assertTrue(bytesCompare(msg.getOptionalBytes_asU8(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes_asB64(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes(), BYTES));
+
+    msg = new proto.jspb.test.TestAllTypes();
+    // Set from a Uint8Array and check all the getters work.
+    msg.setOptionalBytes(BYTES);
+    assertTrue(bytesCompare(msg.getOptionalBytes_asU8(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes_asB64(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes(), BYTES));
+
+  });
+
+  /**
+   * Test that bytes setters will receive result of any of the getters.
+   */
+  it('testBytesFieldsSettersInterop', function() {
+    var msg = new proto.jspb.test.TestAllTypes();
+    msg.setOptionalBytes(BYTES);
+    assertTrue(bytesCompare(msg.getOptionalBytes(), BYTES));
+
+    msg.setOptionalBytes(msg.getOptionalBytes());
+    assertTrue(bytesCompare(msg.getOptionalBytes(), BYTES));
+    msg.setOptionalBytes(msg.getOptionalBytes_asB64());
+    assertTrue(bytesCompare(msg.getOptionalBytes(), BYTES));
+    msg.setOptionalBytes(msg.getOptionalBytes_asU8());
+    assertTrue(bytesCompare(msg.getOptionalBytes(), BYTES));
+  });
+
+  /**
+   * Test that bytes setters will receive result of any of the getters.
+   */
+  it('testRepeatedBytesGetters', function() {
+    var msg = new proto.jspb.test.TestAllTypes();
+
+    function assertGetters() {
+      assertTrue(goog.isString(msg.getRepeatedBytesList_asB64()[0]));
+      assertTrue(goog.isString(msg.getRepeatedBytesList_asB64()[1]));
+      assertTrue(msg.getRepeatedBytesList_asU8()[0] instanceof Uint8Array);
+      assertTrue(msg.getRepeatedBytesList_asU8()[1] instanceof Uint8Array);
+
+      assertTrue(bytesCompare(msg.getRepeatedBytesList()[0], BYTES));
+      assertTrue(bytesCompare(msg.getRepeatedBytesList()[1], BYTES));
+      assertTrue(bytesCompare(msg.getRepeatedBytesList_asB64()[0], BYTES));
+      assertTrue(bytesCompare(msg.getRepeatedBytesList_asB64()[1], BYTES));
+      assertTrue(bytesCompare(msg.getRepeatedBytesList_asU8()[0], BYTES));
+      assertTrue(bytesCompare(msg.getRepeatedBytesList_asU8()[1], BYTES));
+    }
+
+    msg.setRepeatedBytesList([BYTES, BYTES]);
+    assertGetters();
+
+    msg.setRepeatedBytesList([BYTES_B64, BYTES_B64]);
+    assertGetters();
+
+    msg.setRepeatedBytesList(null);
+    assertEquals(0, msg.getRepeatedBytesList().length);
+    assertEquals(0, msg.getRepeatedBytesList_asB64().length);
+    assertEquals(0, msg.getRepeatedBytesList_asU8().length);
+  });
 
   /**
    * Helper: fill all extension values.
@@ -397,8 +535,7 @@
         proto.jspb.test.extendOptionalBool, true);
     msg.setExtension(
         proto.jspb.test.extendOptionalString, 'hello world');
-    msg.setExtension(
-        proto.jspb.test.extendOptionalBytes, 'bytes');
+    msg.setExtension(proto.jspb.test.extendOptionalBytes, BYTES);
     var submsg = new proto.jspb.test.ExtendsWithMessage();
     submsg.setFoo(16);
     msg.setExtension(
@@ -407,6 +544,7 @@
         proto.jspb.test.extendOptionalForeignEnum,
         proto.jspb.test.ForeignEnum.FOREIGN_FOO);
 
+
     msg.setExtension(
         proto.jspb.test.extendRepeatedInt32List, [-42]);
     msg.setExtension(
@@ -435,8 +573,7 @@
         proto.jspb.test.extendRepeatedBoolList, [true]);
     msg.setExtension(
         proto.jspb.test.extendRepeatedStringList, ['hello world']);
-    msg.setExtension(
-        proto.jspb.test.extendRepeatedBytesList, ['bytes']);
+    msg.setExtension(proto.jspb.test.extendRepeatedBytesList, [BYTES]);
     submsg = new proto.jspb.test.ExtendsWithMessage();
     submsg.setFoo(1000);
     msg.setExtension(
@@ -444,6 +581,7 @@
     msg.setExtension(proto.jspb.test.extendRepeatedForeignEnumList,
         [proto.jspb.test.ForeignEnum.FOREIGN_FOO]);
 
+
     msg.setExtension(
         proto.jspb.test.extendPackedRepeatedInt32List, [-42]);
     msg.setExtension(
@@ -473,6 +611,7 @@
         proto.jspb.test.extendPackedRepeatedBoolList, [true]);
     msg.setExtension(proto.jspb.test.extendPackedRepeatedForeignEnumList,
         [proto.jspb.test.ForeignEnum.FOREIGN_FOO]);
+
   }
 
 
diff --git a/js/binary/reader.js b/js/binary/reader.js
index abcd166..15f9043 100644
--- a/js/binary/reader.js
+++ b/js/binary/reader.js
@@ -180,7 +180,7 @@
 
 /**
  * Returns the raw buffer.
- * @return {Uint8Array} The raw buffer.
+ * @return {?Uint8Array} The raw buffer.
  */
 jspb.BinaryReader.prototype.getBuffer = function() {
   return this.decoder_.getBuffer();
@@ -592,8 +592,8 @@
   var start = this.decoder_.getCursor();
   var end = start + length;
 
-  var innerDecoder = jspb.BinaryDecoder.alloc(this.decoder_.getBuffer(),
-                                                 start, length);
+  var innerDecoder =
+      jspb.BinaryDecoder.alloc(this.decoder_.getBuffer(), start, length);
   this.decoder_.setCursor(end);
   return innerDecoder;
 };
@@ -869,7 +869,7 @@
  * Reads a length-prefixed block of bytes from the binary stream, or returns
  * null if the next field in the stream has an invalid length value.
  *
- * @return {Uint8Array} The block of bytes.
+ * @return {!Uint8Array} The block of bytes.
  */
 jspb.BinaryReader.prototype.readBytes = function() {
   goog.asserts.assert(
diff --git a/js/binary/reader_test.js b/js/binary/reader_test.js
index a648261..6f7e5d4 100644
--- a/js/binary/reader_test.js
+++ b/js/binary/reader_test.js
@@ -366,28 +366,28 @@
     var writer = new jspb.BinaryWriter();
 
     var testSignedData = [
-        '2730538252207801776',
-        '-2688470994844604560',
-        '3398529779486536359',
-        '3568577411627971000',
-        '272477188847484900',
-        '-6649058714086158188',
-        '-7695254765712060806',
-        '-4525541438037104029',
-        '-4993706538836508568',
-        '4990160321893729138'
+      '2730538252207801776',
+      '-2688470994844604560',
+      '3398529779486536359',
+      '3568577411627971000',
+      '272477188847484900',
+      '-6649058714086158188',
+      '-7695254765712060806',
+      '-4525541438037104029',
+      '-4993706538836508568',
+      '4990160321893729138'
     ];
     var testUnsignedData = [
-        '7822732630241694882',
-        '6753602971916687352',
-        '2399935075244442116',
-        '8724292567325338867',
-        '16948784802625696584',
-        '4136275908516066934',
-        '3575388346793700364',
-        '5167142028379259461',
-        '1557573948689737699',
-        '17100725280812548567'
+      '7822732630241694882',
+      '6753602971916687352',
+      '2399935075244442116',
+      '8724292567325338867',
+      '16948784802625696584',
+      '4136275908516066934',
+      '3575388346793700364',
+      '5167142028379259461',
+      '1557573948689737699',
+      '17100725280812548567'
     ];
 
     for (var i = 0; i < testSignedData.length; i++) {
@@ -535,7 +535,7 @@
    */
   it('testNesting', function() {
     var writer = new jspb.BinaryWriter();
-  var dummyMessage = /** @type {!jspb.BinaryMessage} */({});
+    var dummyMessage = /** @type {!jspb.BinaryMessage} */({});
 
     writer.writeInt32(1, 100);
 
@@ -626,31 +626,15 @@
     writer.writeBytes(4, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
     writer.writeString(4, 'The quick brown fox jumps over the lazy dog');
 
-    // Write a group with a nested group inside. We use the internal
-    // .rawWriteVarint() to ensure the tested wire data is what we want,
-    // independently of any serialization logic.
+    // Write a group with a nested group inside.
     writer.writeInt32(5, sentinel);
-    // Start group, field 5.
-    writer.rawWriteVarint(
-        (5 << 3) + jspb.BinaryConstants.WireType.START_GROUP);
-    // Varint, field 42.
-    writer.rawWriteVarint(
-        (42 << 3) + jspb.BinaryConstants.WireType.VARINT);
-    // Varint data.
-    writer.rawWriteVarint(42);
-    // Start group, field 6.
-    writer.rawWriteVarint(
-        (6 << 3) + jspb.BinaryConstants.WireType.START_GROUP);
-    // Varint, field 84.
-    writer.rawWriteVarint(
-        (84 << 3) + jspb.BinaryConstants.WireType.VARINT);
-    writer.rawWriteVarint(42);
-    // End group, field 6.
-    writer.rawWriteVarint(
-        (6 << 3) + jspb.BinaryConstants.WireType.END_GROUP);
-    // End group, field 5.
-    writer.rawWriteVarint(
-        (5 << 3) + jspb.BinaryConstants.WireType.END_GROUP);
+    var dummyMessage = /** @type {!jspb.BinaryMessage} */({});
+    writer.writeGroup(5, dummyMessage, function() {
+      writer.writeInt64(42, 42);
+      writer.writeGroup(6, dummyMessage, function() {
+        writer.writeInt64(84, 42);
+      });
+    });
 
     // Write final sentinel.
     writer.writeInt32(6, sentinel);
diff --git a/js/binary/utils.js b/js/binary/utils.js
index 9260038..875ff95 100644
--- a/js/binary/utils.js
+++ b/js/binary/utils.js
@@ -839,62 +839,16 @@
 
 
 /**
- * Clones a scalar field. Pulling this out to a helper method saves us a few
- * bytes of generated code.
- * @param {Array} array
- * @return {Array}
- */
-jspb.utils.cloneRepeatedScalarField = function(array) {
-  return array ? array.slice() : null;
-};
-
-
-/**
- * Clones an array of messages using the provided cloner function.
- * @param {Array.<jspb.BinaryMessage>} messages
- * @param {jspb.ClonerFunction} cloner
- * @return {Array.<jspb.BinaryMessage>}
- */
-jspb.utils.cloneRepeatedMessageField = function(messages, cloner) {
-  if (messages === null) return null;
-  var result = [];
-  for (var i = 0; i < messages.length; i++) {
-    result.push(cloner(messages[i]));
-  }
-  return result;
-};
-
-
-/**
- * Clones an array of byte blobs.
- * @param {Array.<Uint8Array>} blobs
- * @return {Array.<Uint8Array>}
- */
-jspb.utils.cloneRepeatedBlobField = function(blobs) {
-  if (blobs === null) return null;
-  var result = [];
-  for (var i = 0; i < blobs.length; i++) {
-    result.push(new Uint8Array(blobs[i]));
-  }
-  return result;
-};
-
-
-/**
  * String-ify bytes for text format. Should be optimized away in non-debug.
  * The returned string uses \xXX escapes for all values and is itself quoted.
  * [1, 31] serializes to '"\x01\x1f"'.
  * @param {jspb.ByteSource} byteSource The bytes to serialize.
- * @param {boolean=} opt_stringIsRawBytes The string is interpreted as a series
- * of raw bytes rather than base64 data.
  * @return {string} Stringified bytes for text format.
  */
-jspb.utils.debugBytesToTextFormat = function(byteSource,
-                                                opt_stringIsRawBytes) {
+jspb.utils.debugBytesToTextFormat = function(byteSource) {
   var s = '"';
   if (byteSource) {
-    var bytes =
-        jspb.utils.byteSourceToUint8Array(byteSource, opt_stringIsRawBytes);
+    var bytes = jspb.utils.byteSourceToUint8Array(byteSource);
     for (var i = 0; i < bytes.length; i++) {
       s += '\\x';
       if (bytes[i] < 16) s += '0';
@@ -925,9 +879,8 @@
  * exception.
  * @param {string} str
  * @return {!Uint8Array}
- * @private
  */
-jspb.utils.stringToByteArray_ = function(str) {
+jspb.utils.stringToByteArray = function(str) {
   var arr = new Uint8Array(str.length);
   for (var i = 0; i < str.length; i++) {
     var codepoint = str.charCodeAt(i);
@@ -944,13 +897,10 @@
 /**
  * Converts any type defined in jspb.ByteSource into a Uint8Array.
  * @param {!jspb.ByteSource} data
- * @param {boolean=} opt_stringIsRawBytes Interpret a string as a series of raw
- * bytes (encoded as codepoints 0--255 inclusive) rather than base64 data
- * (default behavior).
  * @return {!Uint8Array}
  * @suppress {invalidCasts}
  */
-jspb.utils.byteSourceToUint8Array = function(data, opt_stringIsRawBytes) {
+jspb.utils.byteSourceToUint8Array = function(data) {
   if (data.constructor === Uint8Array) {
     return /** @type {!Uint8Array} */(data);
   }
@@ -967,11 +917,7 @@
 
   if (data.constructor === String) {
     data = /** @type {string} */(data);
-    if (opt_stringIsRawBytes) {
-      return jspb.utils.stringToByteArray_(data);
-    } else {
-      return goog.crypt.base64.decodeStringToUint8Array(data);
-    }
+    return goog.crypt.base64.decodeStringToUint8Array(data);
   }
 
   goog.asserts.fail('Type not convertible to Uint8Array.');
diff --git a/js/binary/utils_test.js b/js/binary/utils_test.js
index 5c33079..518d759 100644
--- a/js/binary/utils_test.js
+++ b/js/binary/utils_test.js
@@ -310,7 +310,7 @@
     // NaN.
     jspb.utils.splitFloat32(NaN);
     if (!isNaN(jspb.utils.joinFloat32(jspb.utils.split64Low,
-                                         jspb.utils.split64High))) {
+                                      jspb.utils.split64High))) {
       throw 'fail!';
     }
 
@@ -324,7 +324,7 @@
         if (opt_bits != jspb.utils.split64Low) throw 'fail!';
       }
       if (truncate(x) != jspb.utils.joinFloat32(jspb.utils.split64Low,
-                                                   jspb.utils.split64High)) {
+          jspb.utils.split64High)) {
         throw 'fail!';
       }
     }
@@ -376,7 +376,7 @@
     // NaN.
     jspb.utils.splitFloat64(NaN);
     if (!isNaN(jspb.utils.joinFloat64(jspb.utils.split64Low,
-                                         jspb.utils.split64High))) {
+        jspb.utils.split64High))) {
       throw 'fail!';
     }
 
@@ -394,7 +394,7 @@
         if (opt_lowBits != jspb.utils.split64Low) throw 'fail!';
       }
       if (x != jspb.utils.joinFloat64(jspb.utils.split64Low,
-                                         jspb.utils.split64High)) {
+          jspb.utils.split64High)) {
         throw 'fail!';
       }
     }
@@ -439,16 +439,20 @@
    * Tests counting packed varints.
    */
   it('testCountVarints', function() {
-    var writer = new jspb.BinaryWriter();
-
-    var count = 0;
+    var values = [];
     for (var i = 1; i < 1000000000; i *= 1.1) {
-      writer.rawWriteVarint(Math.floor(i));
-      count++;
+      values.push(Math.floor(i));
     }
 
+    var writer = new jspb.BinaryWriter();
+    writer.writePackedUint64(1, values);
+
     var buffer = new Uint8Array(writer.getResultBuffer());
-    assertEquals(count, jspb.utils.countVarints(buffer, 0, buffer.length));
+
+    // We should have two more varints than we started with - one for the field
+    // tag, one for the packed length.
+    assertEquals(values.length + 2,
+                 jspb.utils.countVarints(buffer, 0, buffer.length));
   });
 
 
@@ -625,8 +629,5 @@
 
     // Converting base64-encoded strings into Uint8Arrays should work.
     check(convert(sourceBase64));
-
-    // Converting binary-data strings into Uint8Arrays should work.
-    check(convert(sourceString, /* opt_stringIsRawBytes = */ true));
   });
 });
diff --git a/js/binary/writer.js b/js/binary/writer.js
index a184945..be4478e 100644
--- a/js/binary/writer.js
+++ b/js/binary/writer.js
@@ -60,12 +60,11 @@
 goog.require('goog.asserts');
 goog.require('goog.crypt.base64');
 goog.require('jspb.BinaryConstants');
+goog.require('jspb.BinaryEncoder');
 goog.require('jspb.arith.Int64');
 goog.require('jspb.arith.UInt64');
 goog.require('jspb.utils');
 
-goog.forwardDeclare('jspb.Message');
-
 
 
 /**
@@ -84,116 +83,109 @@
   this.blocks_ = [];
 
   /**
-   * Total number of bytes in the blocks_ array. Does _not_ include the temp
-   * buffer.
+   * Total number of bytes in the blocks_ array. Does _not_ include bytes in
+   * the encoder below.
    * @private {number}
    */
   this.totalLength_ = 0;
 
   /**
-   * Temporary buffer holding a message that we're still serializing. When we
-   * get to a stopping point (either the start of a new submessage, or when we
-   * need to append a raw Uint8Array), the temp buffer will be added to the
-   * block array above and a new temp buffer will be created.
-   * @private {!Array.<number>}
+   * Binary encoder holding pieces of a message that we're still serializing.
+   * When we get to a stopping point (either the start of a new submessage, or
+   * when we need to append a raw Uint8Array), the encoder's buffer will be
+   * added to the block array above and the encoder will be reset.
+   * @private {!jspb.BinaryEncoder}
    */
-  this.temp_ = [];
+  this.encoder_ = new jspb.BinaryEncoder();
 
   /**
    * A stack of bookmarks containing the parent blocks for each message started
    * via beginSubMessage(), needed as bookkeeping for endSubMessage().
    * TODO(aappleby): Deprecated, users should be calling writeMessage().
-   * @private {!Array.<!jspb.BinaryWriter.Bookmark_>}
+   * @private {!Array.<!Array.<number>>}
    */
   this.bookmarks_ = [];
 };
 
 
 /**
- * @typedef {{block: !Array.<number>, length: number}}
- * @private
- */
-jspb.BinaryWriter.Bookmark_;
-
-
-/**
- * Saves the current temp buffer in the blocks_ array and starts a new one.
- * @return {!Array.<number>} Returns a reference to the old temp buffer.
- * @private
- */
-jspb.BinaryWriter.prototype.saveTempBuffer_ = function() {
-  var oldTemp = this.temp_;
-  this.blocks_.push(this.temp_);
-  this.totalLength_ += this.temp_.length;
-  this.temp_ = [];
-  return oldTemp;
-};
-
-
-/**
  * Append a typed array of bytes onto the buffer.
  *
  * @param {!Uint8Array} arr The byte array to append.
  * @private
  */
 jspb.BinaryWriter.prototype.appendUint8Array_ = function(arr) {
-  if (this.temp_.length) {
-    this.saveTempBuffer_();
-  }
+  var temp = this.encoder_.end();
+  this.blocks_.push(temp);
   this.blocks_.push(arr);
-  this.totalLength_ += arr.length;
+  this.totalLength_ += temp.length + arr.length;
 };
 
 
 /**
- * Append an untyped array of bytes onto the buffer.
- *
- * @param {!Array.<number>} arr The byte array to append.
- * @private
- */
-jspb.BinaryWriter.prototype.appendArray_ = function(arr) {
-  if (this.temp_.length) {
-    this.saveTempBuffer_();
-  }
-  this.temp_ = arr;
-};
-
-
-/**
- * Begins a length-delimited section by writing the field header to the current
- * temp buffer and then saving it in the block array. Returns the saved block,
- * which we will append the length to in endDelimited_ below.
+ * Begins a new message by writing the field header and returning a bookmark
+ * which we will use to patch in the message length to in endDelimited_ below.
  * @param {number} field
- * @return {!jspb.BinaryWriter.Bookmark_}
+ * @return {!Array.<number>}
  * @private
  */
 jspb.BinaryWriter.prototype.beginDelimited_ = function(field) {
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
-  return {block: this.saveTempBuffer_(), length: this.totalLength_};
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
+  var bookmark = this.encoder_.end();
+  this.blocks_.push(bookmark);
+  this.totalLength_ += bookmark.length;
+  bookmark.push(this.totalLength_);
+  return bookmark;
 };
 
 
 /**
- * Ends a length-delimited block by encoding the _change_ in length of the
- * buffer to the parent block and adds the number of bytes needed to encode
- * that length to the total byte length. Note that 'parentLength' _must_ be the
- * total length _after_ the field header was written in beginDelimited_ above.
- * @param {!jspb.BinaryWriter.Bookmark_} bookmark
+ * Ends a message by encoding the _change_ in length of the buffer to the
+ * parent block and adds the number of bytes needed to encode that length to
+ * the total byte length.
+ * @param {!Array.<number>} bookmark
  * @private
  */
 jspb.BinaryWriter.prototype.endDelimited_ = function(bookmark) {
-  var messageLength = this.totalLength_ + this.temp_.length - bookmark.length;
+  var oldLength = bookmark.pop();
+  var messageLength = this.totalLength_ + this.encoder_.length() - oldLength;
   goog.asserts.assert(messageLength >= 0);
 
-  var bytes = 1;
   while (messageLength > 127) {
-    bookmark.block.push((messageLength & 0x7f) | 0x80);
+    bookmark.push((messageLength & 0x7f) | 0x80);
     messageLength = messageLength >>> 7;
-    bytes++;
+    this.totalLength_++;
   }
 
-  bookmark.block.push(messageLength);
-  this.totalLength_ += bytes;
+  bookmark.push(messageLength);
+  this.totalLength_++;
+};
+
+
+/**
+ * Writes a pre-serialized message to the buffer.
+ * @param {!Uint8Array} bytes The array of bytes to write.
+ * @param {number} start The start of the range to write.
+ * @param {number} end The end of the range to write.
+ */
+jspb.BinaryWriter.prototype.writeSerializedMessage = function(
+    bytes, start, end) {
+  this.appendUint8Array_(bytes.subarray(start, end));
+};
+
+
+/**
+ * Writes a pre-serialized message to the buffer if the message and endpoints
+ * are non-null.
+ * @param {?Uint8Array} bytes The array of bytes to write.
+ * @param {?number} start The start of the range to write.
+ * @param {?number} end The end of the range to write.
+ */
+jspb.BinaryWriter.prototype.maybeWriteSerializedMessage = function(
+    bytes, start, end) {
+  if (bytes != null && start != null && end != null) {
+    this.writeSerializedMessage(bytes, start, end);
+  }
 };
 
 
@@ -202,7 +194,7 @@
  */
 jspb.BinaryWriter.prototype.reset = function() {
   this.blocks_ = [];
-  this.temp_ = [];
+  this.encoder_.end();
   this.totalLength_ = 0;
   this.bookmarks_ = [];
 };
@@ -215,7 +207,7 @@
 jspb.BinaryWriter.prototype.getResultBuffer = function() {
   goog.asserts.assert(this.bookmarks_.length == 0);
 
-  var flat = new Uint8Array(this.totalLength_ + this.temp_.length);
+  var flat = new Uint8Array(this.totalLength_ + this.encoder_.length());
 
   var blocks = this.blocks_;
   var blockCount = blocks.length;
@@ -227,8 +219,9 @@
     offset += block.length;
   }
 
-  flat.set(this.temp_, offset);
-  offset += this.temp_.length;
+  var tail = this.encoder_.end();
+  flat.set(tail, offset);
+  offset += tail.length;
 
   // Post condition: `flattened` must have had every byte written.
   goog.asserts.assert(offset == flat.length);
@@ -236,7 +229,6 @@
   // Replace our block list with the flattened block, which lets GC reclaim
   // the temp blocks sooner.
   this.blocks_ = [flat];
-  this.temp_ = [];
 
   return flat;
 };
@@ -273,331 +265,6 @@
 
 
 /**
- * Encodes a 32-bit unsigned integer into its wire-format varint representation
- * and stores it in the buffer.
- * @param {number} value The integer to convert.
- */
-jspb.BinaryWriter.prototype.rawWriteUnsignedVarint32 = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-
-  while (value > 127) {
-    this.temp_.push((value & 0x7f) | 0x80);
-    value = value >>> 7;
-  }
-
-  this.temp_.push(value);
-};
-
-
-/**
- * Encodes a 32-bit signed integer into its wire-format varint representation
- * and stores it in the buffer.
- * @param {number} value The integer to convert.
- */
-jspb.BinaryWriter.prototype.rawWriteSignedVarint32 = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  if (value >= 0) {
-    this.rawWriteUnsignedVarint32(value);
-    return;
-  }
-
-  // Write nine bytes with a _signed_ right shift so we preserve the sign bit.
-  for (var i = 0; i < 9; i++) {
-    this.temp_.push((value & 0x7f) | 0x80);
-    value = value >> 7;
-  }
-
-  // The above loop writes out 63 bits, so the last byte is always the sign bit
-  // which is always set for negative numbers.
-  this.temp_.push(1);
-};
-
-
-/**
- * Encodes an unsigned 64-bit integer in 32:32 split representation into its
- * wire-format varint representation and stores it in the buffer.
- * @param {number} lowBits The low 32 bits of the int.
- * @param {number} highBits The high 32 bits of the int.
- */
-jspb.BinaryWriter.prototype.rawWriteSplitVarint =
-    function(lowBits, highBits) {
-  // Break the binary representation into chunks of 7 bits, set the 8th bit
-  // in each chunk if it's not the final chunk, and append to the result.
-  while (highBits > 0 || lowBits > 127) {
-    this.temp_.push((lowBits & 0x7f) | 0x80);
-    lowBits = ((lowBits >>> 7) | (highBits << 25)) >>> 0;
-    highBits = highBits >>> 7;
-  }
-  this.temp_.push(lowBits);
-};
-
-
-/**
- * Encodes a JavaScript integer into its wire-format varint representation and
- * stores it in the buffer. Due to the way the varint encoding works this
- * behaves correctly for both signed and unsigned integers, though integers
- * that are not representable in 64 bits will still be truncated.
- * @param {number} value The integer to convert.
- */
-jspb.BinaryWriter.prototype.rawWriteVarint = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  jspb.utils.splitInt64(value);
-  this.rawWriteSplitVarint(jspb.utils.split64Low,
-                           jspb.utils.split64High);
-};
-
-
-/**
- * Encodes a jspb.arith.{Int64,UInt64} instance into its wire-format
- * varint representation and stores it in the buffer. Due to the way the varint
- * encoding works this behaves correctly for both signed and unsigned integers,
- * though integers that are not representable in 64 bits will still be
- * truncated.
- * @param {jspb.arith.Int64|jspb.arith.UInt64} value
- */
-jspb.BinaryWriter.prototype.rawWriteVarintFromNum = function(value) {
-  this.rawWriteSplitVarint(value.lo, value.hi);
-};
-
-
-/**
- * Encodes a JavaScript integer into its wire-format, zigzag-encoded varint
- * representation and stores it in the buffer.
- * @param {number} value The integer to convert.
- */
-jspb.BinaryWriter.prototype.rawWriteZigzagVarint32 = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  this.rawWriteUnsignedVarint32(((value << 1) ^ (value >> 31)) >>> 0);
-};
-
-
-/**
- * Encodes a JavaScript integer into its wire-format, zigzag-encoded varint
- * representation and stores it in the buffer. Integers not representable in 64
- * bits will be truncated.
- * @param {number} value The integer to convert.
- */
-jspb.BinaryWriter.prototype.rawWriteZigzagVarint = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  jspb.utils.splitZigzag64(value);
-  this.rawWriteSplitVarint(jspb.utils.split64Low,
-                           jspb.utils.split64High);
-};
-
-
-/**
- * Writes a raw 8-bit unsigned integer to the buffer. Numbers outside the range
- * [0,2^8) will be truncated.
- * @param {number} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteUint8 = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  goog.asserts.assert((value >= 0) && (value < 256));
-  this.temp_.push((value >>> 0) & 0xFF);
-};
-
-
-/**
- * Writes a raw 16-bit unsigned integer to the buffer. Numbers outside the
- * range [0,2^16) will be truncated.
- * @param {number} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteUint16 = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  goog.asserts.assert((value >= 0) && (value < 65536));
-  this.temp_.push((value >>> 0) & 0xFF);
-  this.temp_.push((value >>> 8) & 0xFF);
-};
-
-
-/**
- * Writes a raw 32-bit unsigned integer to the buffer. Numbers outside the
- * range [0,2^32) will be truncated.
- * @param {number} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteUint32 = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  goog.asserts.assert((value >= 0) &&
-                      (value < jspb.BinaryConstants.TWO_TO_32));
-  this.temp_.push((value >>> 0) & 0xFF);
-  this.temp_.push((value >>> 8) & 0xFF);
-  this.temp_.push((value >>> 16) & 0xFF);
-  this.temp_.push((value >>> 24) & 0xFF);
-};
-
-
-/**
- * Writes a raw 64-bit unsigned integer to the buffer. Numbers outside the
- * range [0,2^64) will be truncated.
- * @param {number} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteUint64 = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  goog.asserts.assert((value >= 0) &&
-                      (value < jspb.BinaryConstants.TWO_TO_64));
-  jspb.utils.splitUint64(value);
-  this.rawWriteUint32(jspb.utils.split64Low);
-  this.rawWriteUint32(jspb.utils.split64High);
-};
-
-
-/**
- * Writes a raw 8-bit integer to the buffer. Numbers outside the range
- * [-2^7,2^7) will be truncated.
- * @param {number} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteInt8 = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  goog.asserts.assert((value >= -128) && (value < 128));
-  this.temp_.push((value >>> 0) & 0xFF);
-};
-
-
-/**
- * Writes a raw 16-bit integer to the buffer. Numbers outside the range
- * [-2^15,2^15) will be truncated.
- * @param {number} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteInt16 = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  goog.asserts.assert((value >= -32768) && (value < 32768));
-  this.temp_.push((value >>> 0) & 0xFF);
-  this.temp_.push((value >>> 8) & 0xFF);
-};
-
-
-/**
- * Writes a raw 32-bit integer to the buffer. Numbers outside the range
- * [-2^31,2^31) will be truncated.
- * @param {number} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteInt32 = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_31) &&
-                      (value < jspb.BinaryConstants.TWO_TO_31));
-  this.temp_.push((value >>> 0) & 0xFF);
-  this.temp_.push((value >>> 8) & 0xFF);
-  this.temp_.push((value >>> 16) & 0xFF);
-  this.temp_.push((value >>> 24) & 0xFF);
-};
-
-
-/**
- * Writes a raw 64-bit integer to the buffer. Numbers outside the range
- * [-2^63,2^63) will be truncated.
- * @param {number} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteInt64 = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_63) &&
-                      (value < jspb.BinaryConstants.TWO_TO_63));
-  jspb.utils.splitInt64(value);
-  this.rawWriteUint32(jspb.utils.split64Low);
-  this.rawWriteUint32(jspb.utils.split64High);
-};
-
-
-/**
- * Writes a raw single-precision floating point value to the buffer. Numbers
- * requiring more than 32 bits of precision will be truncated.
- * @param {number} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteFloat = function(value) {
-  jspb.utils.splitFloat32(value);
-  this.rawWriteUint32(jspb.utils.split64Low);
-};
-
-
-/**
- * Writes a raw double-precision floating point value to the buffer. As this is
- * the native format used by JavaScript, no precision will be lost.
- * @param {number} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteDouble = function(value) {
-  jspb.utils.splitFloat64(value);
-  this.rawWriteUint32(jspb.utils.split64Low);
-  this.rawWriteUint32(jspb.utils.split64High);
-};
-
-
-/**
- * Writes a raw boolean value to the buffer as a varint.
- * @param {boolean} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteBool = function(value) {
-  goog.asserts.assert(goog.isBoolean(value));
-  this.temp_.push(~~value);
-};
-
-
-/**
- * Writes an raw enum value to the buffer as a varint.
- * @param {number} value The value to write.
- */
-jspb.BinaryWriter.prototype.rawWriteEnum = function(value) {
-  goog.asserts.assert(value == Math.floor(value));
-  goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_31) &&
-                      (value < jspb.BinaryConstants.TWO_TO_31));
-  this.rawWriteSignedVarint32(value);
-};
-
-
-/**
- * Writes a raw string value to the buffer.
- * @param {string} string The string to write.
- */
-jspb.BinaryWriter.prototype.rawWriteUtf8String = function(string) {
-  for (var i = 0; i < string.length; i++) {
-    this.temp_.push(string.charCodeAt(i));
-  }
-};
-
-
-/**
- * Writes an arbitrary raw byte array to the buffer.
- * @param {!Uint8Array} bytes The array of bytes to write.
- */
-jspb.BinaryWriter.prototype.rawWriteBytes = function(bytes) {
-  this.appendUint8Array_(bytes);
-};
-
-
-/**
- * Writes an arbitrary raw byte array to the buffer.
- * @param {!Uint8Array} bytes The array of bytes to write.
- * @param {number} start The start of the range to write.
- * @param {number} end The end of the range to write.
- */
-jspb.BinaryWriter.prototype.rawWriteByteRange = function(bytes, start, end) {
-  this.appendUint8Array_(bytes.subarray(start, end));
-};
-
-
-/**
- * Writes a 64-bit hash string (8 characters @ 8 bits of data each) to the
- * buffer as a varint.
- * @param {string} hash The hash to write.
- */
-jspb.BinaryWriter.prototype.rawWriteVarintHash64 = function(hash) {
-  jspb.utils.splitHash64(hash);
-  this.rawWriteSplitVarint(jspb.utils.split64Low,
-                           jspb.utils.split64High);
-};
-
-
-/**
- * Writes a 64-bit hash string (8 characters @ 8 bits of data each) to the
- * buffer as a fixed64.
- * @param {string} hash The hash to write.
- */
-jspb.BinaryWriter.prototype.rawWriteFixedHash64 = function(hash) {
-  jspb.utils.splitHash64(hash);
-  this.rawWriteUint32(jspb.utils.split64Low);
-  this.rawWriteUint32(jspb.utils.split64High);
-};
-
-
-/**
  * Encodes a (field number, wire type) tuple into a wire-format field header
  * and stores it in the buffer as a varint.
  * @param {number} field The field number.
@@ -605,11 +272,11 @@
  *     protocol buffer documentation.
  * @private
  */
-jspb.BinaryWriter.prototype.rawWriteFieldHeader_ =
+jspb.BinaryWriter.prototype.writeFieldHeader_ =
     function(field, wireType) {
   goog.asserts.assert(field >= 1 && field == Math.floor(field));
   var x = field * 8 + wireType;
-  this.rawWriteUnsignedVarint32(x);
+  this.encoder_.writeUnsignedVarint32(x);
 };
 
 
@@ -697,8 +364,8 @@
  */
 jspb.BinaryWriter.prototype.writeUnsignedVarint32_ = function(field, value) {
   if (value == null) return;
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
-  this.rawWriteSignedVarint32(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeUnsignedVarint32(value);
 };
 
 
@@ -710,8 +377,8 @@
  */
 jspb.BinaryWriter.prototype.writeSignedVarint32_ = function(field, value) {
   if (value == null) return;
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
-  this.rawWriteSignedVarint32(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeSignedVarint32(value);
 };
 
 
@@ -721,10 +388,23 @@
  * @param {number?} value The value to write.
  * @private
  */
-jspb.BinaryWriter.prototype.writeVarint_ = function(field, value) {
+jspb.BinaryWriter.prototype.writeUnsignedVarint64_ = function(field, value) {
   if (value == null) return;
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
-  this.rawWriteVarint(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeUnsignedVarint64(value);
+};
+
+
+/**
+ * Writes a varint field to the buffer without range checking.
+ * @param {number} field The field number.
+ * @param {number?} value The value to write.
+ * @private
+ */
+jspb.BinaryWriter.prototype.writeSignedVarint64_ = function(field, value) {
+  if (value == null) return;
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeSignedVarint64(value);
 };
 
 
@@ -736,8 +416,8 @@
  */
 jspb.BinaryWriter.prototype.writeZigzagVarint32_ = function(field, value) {
   if (value == null) return;
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
-  this.rawWriteZigzagVarint32(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeZigzagVarint32(value);
 };
 
 
@@ -747,10 +427,10 @@
  * @param {number?} value The value to write.
  * @private
  */
-jspb.BinaryWriter.prototype.writeZigzagVarint_ = function(field, value) {
+jspb.BinaryWriter.prototype.writeZigzagVarint64_ = function(field, value) {
   if (value == null) return;
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
-  this.rawWriteZigzagVarint(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeZigzagVarint64(value);
 };
 
 
@@ -793,7 +473,7 @@
   if (value == null) return;
   goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_63) &&
                       (value < jspb.BinaryConstants.TWO_TO_63));
-  this.writeVarint_(field, value);
+  this.writeSignedVarint64_(field, value);
 };
 
 
@@ -805,8 +485,8 @@
 jspb.BinaryWriter.prototype.writeInt64String = function(field, value) {
   if (value == null) return;
   var num = jspb.arith.Int64.fromString(value);
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
-  this.rawWriteVarintFromNum(num);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeSplitVarint64(num.lo, num.hi);
 };
 
 
@@ -849,7 +529,7 @@
   if (value == null) return;
   goog.asserts.assert((value >= 0) &&
                       (value < jspb.BinaryConstants.TWO_TO_64));
-  this.writeVarint_(field, value);
+  this.writeUnsignedVarint64_(field, value);
 };
 
 
@@ -861,8 +541,8 @@
 jspb.BinaryWriter.prototype.writeUint64String = function(field, value) {
   if (value == null) return;
   var num = jspb.arith.UInt64.fromString(value);
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
-  this.rawWriteVarintFromNum(num);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeSplitVarint64(num.lo, num.hi);
 };
 
 
@@ -890,7 +570,7 @@
   if (value == null) return;
   goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_63) &&
                       (value < jspb.BinaryConstants.TWO_TO_63));
-  this.writeZigzagVarint_(field, value);
+  this.writeZigzagVarint64_(field, value);
 };
 
 
@@ -904,8 +584,8 @@
   if (value == null) return;
   goog.asserts.assert((value >= 0) &&
                       (value < jspb.BinaryConstants.TWO_TO_32));
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED32);
-  this.rawWriteUint32(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED32);
+  this.encoder_.writeUint32(value);
 };
 
 
@@ -919,8 +599,8 @@
   if (value == null) return;
   goog.asserts.assert((value >= 0) &&
                       (value < jspb.BinaryConstants.TWO_TO_64));
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED64);
-  this.rawWriteUint64(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED64);
+  this.encoder_.writeUint64(value);
 };
 
 
@@ -934,8 +614,8 @@
   if (value == null) return;
   goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_31) &&
                       (value < jspb.BinaryConstants.TWO_TO_31));
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED32);
-  this.rawWriteInt32(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED32);
+  this.encoder_.writeInt32(value);
 };
 
 
@@ -949,8 +629,8 @@
   if (value == null) return;
   goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_63) &&
                       (value < jspb.BinaryConstants.TWO_TO_63));
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED64);
-  this.rawWriteInt64(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED64);
+  this.encoder_.writeInt64(value);
 };
 
 
@@ -962,8 +642,8 @@
  */
 jspb.BinaryWriter.prototype.writeFloat = function(field, value) {
   if (value == null) return;
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED32);
-  this.rawWriteFloat(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED32);
+  this.encoder_.writeFloat(value);
 };
 
 
@@ -975,8 +655,8 @@
  */
 jspb.BinaryWriter.prototype.writeDouble = function(field, value) {
   if (value == null) return;
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED64);
-  this.rawWriteDouble(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED64);
+  this.encoder_.writeDouble(value);
 };
 
 
@@ -988,8 +668,8 @@
 jspb.BinaryWriter.prototype.writeBool = function(field, value) {
   if (value == null) return;
   goog.asserts.assert(goog.isBoolean(value));
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
-  this.temp_.push(~~value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeBool(value);
 };
 
 
@@ -1002,8 +682,8 @@
   if (value == null) return;
   goog.asserts.assert((value >= -jspb.BinaryConstants.TWO_TO_31) &&
                       (value < jspb.BinaryConstants.TWO_TO_31));
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
-  this.rawWriteSignedVarint32(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeSignedVarint32(value);
 };
 
 
@@ -1014,99 +694,41 @@
  */
 jspb.BinaryWriter.prototype.writeString = function(field, value) {
   if (value == null) return;
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
-
-  // Conversion loop swiped from goog.crypt.stringToUtf8ByteArray. Note that
-  // 'bytes' will be at least as long as 'value', but could be longer if we
-  // need to unpack unicode characters.
-  var bytes = [];
-  for (var i = 0; i < value.length; i++) {
-    var c = value.charCodeAt(i);
-    if (c < 128) {
-      bytes.push(c);
-    } else if (c < 2048) {
-      bytes.push((c >> 6) | 192);
-      bytes.push((c & 63) | 128);
-    } else {
-      bytes.push((c >> 12) | 224);
-      bytes.push(((c >> 6) & 63) | 128);
-      bytes.push((c & 63) | 128);
-    }
-  }
-
-  this.rawWriteUnsignedVarint32(bytes.length);
-  this.appendArray_(bytes);
+  var bookmark = this.beginDelimited_(field);
+  this.encoder_.writeString(value);
+  this.endDelimited_(bookmark);
 };
 
 
 /**
  * Writes an arbitrary byte field to the buffer. Note - to match the behavior
  * of the C++ implementation, empty byte arrays _are_ serialized.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
- * @param {jspb.ByteSource} value The array of bytes to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
- * @param {boolean=} opt_stringIsRawBytes If `value` is a string, interpret it
- * as a series of raw bytes (codepoints 0--255 inclusive) rather than base64
- * data.
+ * @param {?jspb.ByteSource} value The array of bytes to write.
  */
-jspb.BinaryWriter.prototype.writeBytes =
-    function(field, value, opt_buffer, opt_start, opt_end,
-             opt_stringIsRawBytes) {
-  if (value != null) {
-    this.rawWriteFieldHeader_(field,
-        jspb.BinaryConstants.WireType.DELIMITED);
-    this.rawWriteUnsignedVarint32(value.length);
-    this.rawWriteBytes(
-        jspb.utils.byteSourceToUint8Array(value, opt_stringIsRawBytes));
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
-  }
-};
-
-
-/**
- * Writes an arbitrary byte field to the buffer, with `opt_stringIsRawBytes`
- * flag implicitly true.
- * @param {number} field
- * @param {jspb.ByteSource} value The array of bytes to write.
- */
-jspb.BinaryWriter.prototype.writeBytesRawString = function(field, value) {
-  this.writeBytes(field, value, null, null, null, true);
+jspb.BinaryWriter.prototype.writeBytes = function(field, value) {
+  if (value == null) return;
+  var bytes = jspb.utils.byteSourceToUint8Array(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
+  this.encoder_.writeUnsignedVarint32(bytes.length);
+  this.appendUint8Array_(bytes);
 };
 
 
 /**
  * Writes a message to the buffer.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @template MessageType
  * @param {number} field The field number.
  * @param {?MessageType} value The message to write.
  * @param {!jspb.WriterFunction} writerCallback Will be invoked with the value
  *     to write and the writer to write it with.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
-jspb.BinaryWriter.prototype.writeMessage =
-    function(field, value, writerCallback, opt_buffer, opt_start, opt_end) {
-  if (value !== null) {
-    var bookmark = this.beginDelimited_(field);
-
-    writerCallback(value, this);
-
-    this.endDelimited_(bookmark);
-  } else if (opt_buffer && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
-  }
+jspb.BinaryWriter.prototype.writeMessage = function(
+    field, value, writerCallback) {
+  if (value == null) return;
+  var bookmark = this.beginDelimited_(field);
+  writerCallback(value, this);
+  this.endDelimited_(bookmark);
 };
 
 
@@ -1120,15 +742,12 @@
  * @param {!jspb.WriterFunction} writerCallback Will be invoked with the value
  *     to write and the writer to write it with.
  */
-jspb.BinaryWriter.prototype.writeGroup =
-    function(field, value, writerCallback) {
-  if (value) {
-    this.rawWriteFieldHeader_(
-        field, jspb.BinaryConstants.WireType.START_GROUP);
-    writerCallback(value, this);
-    this.rawWriteFieldHeader_(
-        field, jspb.BinaryConstants.WireType.END_GROUP);
-  }
+jspb.BinaryWriter.prototype.writeGroup = function(
+    field, value, writerCallback) {
+  if (value == null) return;
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.START_GROUP);
+  writerCallback(value, this);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.END_GROUP);
 };
 
 
@@ -1141,8 +760,8 @@
 jspb.BinaryWriter.prototype.writeFixedHash64 = function(field, value) {
   if (value == null) return;
   goog.asserts.assert(value.length == 8);
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED64);
-  this.rawWriteFixedHash64(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.FIXED64);
+  this.encoder_.writeFixedHash64(value);
 };
 
 
@@ -1155,8 +774,8 @@
 jspb.BinaryWriter.prototype.writeVarintHash64 = function(field, value) {
   if (value == null) return;
   goog.asserts.assert(value.length == 8);
-  this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
-  this.rawWriteVarintHash64(value);
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.VARINT);
+  this.encoder_.writeVarintHash64(value);
 };
 
 
@@ -1196,10 +815,26 @@
  * @param {?Array.<number>} value The array of ints to write.
  * @private
  */
-jspb.BinaryWriter.prototype.writeRepeatedVarint_ = function(field, value) {
+jspb.BinaryWriter.prototype.writeRepeatedUnsignedVarint64_ =
+    function(field, value) {
   if (value == null) return;
   for (var i = 0; i < value.length; i++) {
-    this.writeVarint_(field, value[i]);
+    this.writeUnsignedVarint64_(field, value[i]);
+  }
+};
+
+
+/**
+ * Writes an array of numbers to the buffer as a repeated varint field.
+ * @param {number} field The field number.
+ * @param {?Array.<number>} value The array of ints to write.
+ * @private
+ */
+jspb.BinaryWriter.prototype.writeRepeatedSignedVarint64_ =
+    function(field, value) {
+  if (value == null) return;
+  for (var i = 0; i < value.length; i++) {
+    this.writeSignedVarint64_(field, value[i]);
   }
 };
 
@@ -1227,7 +862,7 @@
 jspb.BinaryWriter.prototype.writeRepeatedZigzag_ = function(field, value) {
   if (value == null) return;
   for (var i = 0; i < value.length; i++) {
-    this.writeZigzagVarint_(field, value[i]);
+    this.writeZigzagVarint64_(field, value[i]);
   }
 };
 
@@ -1262,7 +897,7 @@
  * @param {?Array.<number>} value The array of ints to write.
  */
 jspb.BinaryWriter.prototype.writeRepeatedInt64 =
-    jspb.BinaryWriter.prototype.writeRepeatedVarint_;
+    jspb.BinaryWriter.prototype.writeRepeatedSignedVarint64_;
 
 
 /**
@@ -1312,7 +947,7 @@
  * @param {?Array.<number>} value The array of ints to write.
  */
 jspb.BinaryWriter.prototype.writeRepeatedUint64 =
-    jspb.BinaryWriter.prototype.writeRepeatedVarint_;
+    jspb.BinaryWriter.prototype.writeRepeatedUnsignedVarint64_;
 
 
 /**
@@ -1469,96 +1104,54 @@
 
 /**
  * Writes an array of arbitrary byte fields to the buffer.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
- * @param {?Array.<!Uint8Array|string>} value
- *     The arrays of arrays of bytes to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
- * @param {boolean=} opt_stringIsRawBytes Any values that are strings are
- * interpreted as raw bytes rather than base64 data.
+ * @param {?Array.<!jspb.ByteSource>} value The arrays of arrays of bytes to
+ *     write.
  */
-jspb.BinaryWriter.prototype.writeRepeatedBytes =
-    function(field, value, opt_buffer, opt_start, opt_end,
-             opt_stringIsRawBytes) {
-  if (value != null) {
-    for (var i = 0; i < value.length; i++) {
-      this.writeBytes(field, value[i], null, null, null, opt_stringIsRawBytes);
-    }
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writeRepeatedBytes = function(field, value) {
+  if (value == null) return;
+  for (var i = 0; i < value.length; i++) {
+    this.writeBytes(field, value[i]);
   }
 };
 
 
 /**
- * Writes an array of arbitrary byte fields to the buffer, with
- * `opt_stringIsRawBytes` implicitly true.
- * @param {number} field
- * @param {?Array.<string>} value
- */
-jspb.BinaryWriter.prototype.writeRepeatedBytesRawString =
-    function(field, value) {
-  this.writeRepeatedBytes(field, value, null, null, null, true);
-};
-
-
-/**
  * Writes an array of messages to the buffer.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @template MessageType
  * @param {number} field The field number.
- * @param {?Array.<!MessageType>} value The array of messages to
+ * @param {?Array.<MessageType>} value The array of messages to
  *    write.
  * @param {!jspb.WriterFunction} writerCallback Will be invoked with the value
  *     to write and the writer to write it with.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
-jspb.BinaryWriter.prototype.writeRepeatedMessage =
-    function(field, value, writerCallback, opt_buffer, opt_start, opt_end) {
-  if (value) {
-    for (var i = 0; i < value.length; i++) {
-      var bookmark = this.beginDelimited_(field);
-
-      writerCallback(value[i], this);
-
-      this.endDelimited_(bookmark);
-    }
-  } else if (opt_buffer && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writeRepeatedMessage = function(
+    field, value, writerCallback) {
+  if (value == null) return;
+  for (var i = 0; i < value.length; i++) {
+    var bookmark = this.beginDelimited_(field);
+    writerCallback(value[i], this);
+    this.endDelimited_(bookmark);
   }
 };
 
 
 /**
  * Writes an array of group messages to the buffer.
- *
  * @template MessageType
  * @param {number} field The field number.
- * @param {?Array.<!MessageType>} value The array of messages to
+ * @param {?Array.<MessageType>} value The array of messages to
  *    write.
  * @param {!jspb.WriterFunction} writerCallback Will be invoked with the value
  *     to write and the writer to write it with.
  */
-jspb.BinaryWriter.prototype.writeRepeatedGroup =
-    function(field, value, writerCallback) {
-  if (value) {
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteFieldHeader_(
-          field, jspb.BinaryConstants.WireType.START_GROUP);
-      writerCallback(value[i], this);
-      this.rawWriteFieldHeader_(
-          field, jspb.BinaryConstants.WireType.END_GROUP);
-    }
+jspb.BinaryWriter.prototype.writeRepeatedGroup = function(
+    field, value, writerCallback) {
+  if (value == null) return;
+  for (var i = 0; i < value.length; i++) {
+    this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.START_GROUP);
+    writerCallback(value[i], this);
+    this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.END_GROUP);
   }
 };
 
@@ -1595,123 +1188,108 @@
 
 /**
  * Writes an array of numbers to the buffer as a packed varint field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  * @private
  */
-jspb.BinaryWriter.prototype.writePackedUnsignedVarint32_ =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    var bookmark = this.beginDelimited_(field);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteUnsignedVarint32(value[i]);
-    }
-    this.endDelimited_(bookmark);
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedUnsignedVarint32_ = function(
+    field, value) {
+  if (value == null || !value.length) return;
+  var bookmark = this.beginDelimited_(field);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeUnsignedVarint32(value[i]);
   }
+  this.endDelimited_(bookmark);
 };
 
 
 /**
  * Writes an array of numbers to the buffer as a packed varint field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  * @private
  */
-jspb.BinaryWriter.prototype.writePackedSignedVarint32_ =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    var bookmark = this.beginDelimited_(field);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteSignedVarint32(value[i]);
-    }
-    this.endDelimited_(bookmark);
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedSignedVarint32_ = function(
+    field, value) {
+  if (value == null || !value.length) return;
+  var bookmark = this.beginDelimited_(field);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeSignedVarint32(value[i]);
   }
+  this.endDelimited_(bookmark);
 };
 
 
 /**
  * Writes an array of numbers to the buffer as a packed varint field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  * @private
  */
-jspb.BinaryWriter.prototype.writePackedVarint_ =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    var bookmark = this.beginDelimited_(field);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteVarint(value[i]);
-    }
-    this.endDelimited_(bookmark);
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedUnsignedVarint64_ = function(
+    field, value) {
+  if (value == null || !value.length) return;
+  var bookmark = this.beginDelimited_(field);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeUnsignedVarint64(value[i]);
   }
+  this.endDelimited_(bookmark);
+};
+
+
+/**
+ * Writes an array of numbers to the buffer as a packed varint field.
+ * @param {number} field The field number.
+ * @param {?Array.<number>} value The array of ints to write.
+ * @private
+ */
+jspb.BinaryWriter.prototype.writePackedSignedVarint64_ = function(
+    field, value) {
+  if (value == null || !value.length) return;
+  var bookmark = this.beginDelimited_(field);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeSignedVarint64(value[i]);
+  }
+  this.endDelimited_(bookmark);
 };
 
 
 /**
  * Writes an array of numbers to the buffer as a packed zigzag field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  * @private
  */
-jspb.BinaryWriter.prototype.writePackedZigzag_ =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    var bookmark = this.beginDelimited_(field);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteZigzagVarint(value[i]);
-    }
-    this.endDelimited_(bookmark);
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedZigzag32_ = function(field, value) {
+  if (value == null || !value.length) return;
+  var bookmark = this.beginDelimited_(field);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeZigzagVarint32(value[i]);
   }
+  this.endDelimited_(bookmark);
+};
+
+
+/**
+ * Writes an array of numbers to the buffer as a packed zigzag field.
+ * @param {number} field The field number.
+ * @param {?Array.<number>} value The array of ints to write.
+ * @private
+ */
+jspb.BinaryWriter.prototype.writePackedZigzag64_ = function(field, value) {
+  if (value == null || !value.length) return;
+  var bookmark = this.beginDelimited_(field);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeZigzagVarint64(value[i]);
+  }
+  this.endDelimited_(bookmark);
 };
 
 
 /**
  * Writes an array of numbers to the buffer as a packed 32-bit int field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
 jspb.BinaryWriter.prototype.writePackedInt32 =
     jspb.BinaryWriter.prototype.writePackedSignedVarint32_;
@@ -1727,7 +1305,7 @@
   if (value == null || !value.length) return;
   var bookmark = this.beginDelimited_(field);
   for (var i = 0; i < value.length; i++) {
-    this.rawWriteSignedVarint32(parseInt(value[i], 10));
+    this.encoder_.writeSignedVarint32(parseInt(value[i], 10));
   }
   this.endDelimited_(bookmark);
 };
@@ -1737,12 +1315,9 @@
  * Writes an array of numbers to the buffer as a packed 64-bit int field.
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
 jspb.BinaryWriter.prototype.writePackedInt64 =
-    jspb.BinaryWriter.prototype.writePackedVarint_;
+    jspb.BinaryWriter.prototype.writePackedSignedVarint64_;
 
 
 /**
@@ -1757,7 +1332,7 @@
   var bookmark = this.beginDelimited_(field);
   for (var i = 0; i < value.length; i++) {
     var num = jspb.arith.Int64.fromString(value[i]);
-    this.rawWriteVarintFromNum(num);
+    this.encoder_.writeSplitVarint64(num.lo, num.hi);
   }
   this.endDelimited_(bookmark);
 };
@@ -1765,15 +1340,8 @@
 
 /**
  * Writes an array numbers to the buffer as a packed unsigned 32-bit int field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
 jspb.BinaryWriter.prototype.writePackedUint32 =
     jspb.BinaryWriter.prototype.writePackedUnsignedVarint32_;
@@ -1790,7 +1358,7 @@
   if (value == null || !value.length) return;
   var bookmark = this.beginDelimited_(field);
   for (var i = 0; i < value.length; i++) {
-    this.rawWriteUnsignedVarint32(parseInt(value[i], 10));
+    this.encoder_.writeUnsignedVarint32(parseInt(value[i], 10));
   }
   this.endDelimited_(bookmark);
 };
@@ -1798,18 +1366,11 @@
 
 /**
  * Writes an array numbers to the buffer as a packed unsigned 64-bit int field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
 jspb.BinaryWriter.prototype.writePackedUint64 =
-    jspb.BinaryWriter.prototype.writePackedVarint_;
+    jspb.BinaryWriter.prototype.writePackedUnsignedVarint64_;
 
 
 /**
@@ -1824,7 +1385,7 @@
   var bookmark = this.beginDelimited_(field);
   for (var i = 0; i < value.length; i++) {
     var num = jspb.arith.UInt64.fromString(value[i]);
-    this.rawWriteVarintFromNum(num);
+    this.encoder_.writeSplitVarint64(num.lo, num.hi);
   }
   this.endDelimited_(bookmark);
 };
@@ -1832,240 +1393,154 @@
 
 /**
  * Writes an array numbers to the buffer as a packed signed 32-bit int field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
 jspb.BinaryWriter.prototype.writePackedSint32 =
-    jspb.BinaryWriter.prototype.writePackedZigzag_;
+    jspb.BinaryWriter.prototype.writePackedZigzag32_;
 
 
 /**
  * Writes an array numbers to the buffer as a packed signed 64-bit int field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
 jspb.BinaryWriter.prototype.writePackedSint64 =
-    jspb.BinaryWriter.prototype.writePackedZigzag_;
+    jspb.BinaryWriter.prototype.writePackedZigzag64_;
 
 
 /**
  * Writes an array of numbers to the buffer as a packed fixed32 field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
-jspb.BinaryWriter.prototype.writePackedFixed32 =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
-    this.rawWriteUnsignedVarint32(value.length * 4);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteUint32(value[i]);
-    }
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedFixed32 = function(field, value) {
+  if (value == null || !value.length) return;
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
+  this.encoder_.writeUnsignedVarint32(value.length * 4);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeUint32(value[i]);
   }
 };
 
 
 /**
  * Writes an array of numbers to the buffer as a packed fixed64 field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
-jspb.BinaryWriter.prototype.writePackedFixed64 =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
-    this.rawWriteUnsignedVarint32(value.length * 8);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteUint64(value[i]);
-    }
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedFixed64 = function(field, value) {
+  if (value == null || !value.length) return;
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
+  this.encoder_.writeUnsignedVarint32(value.length * 8);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeUint64(value[i]);
   }
 };
 
 
 /**
  * Writes an array of numbers to the buffer as a packed sfixed32 field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
-jspb.BinaryWriter.prototype.writePackedSfixed32 =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
-    this.rawWriteUnsignedVarint32(value.length * 4);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteInt32(value[i]);
-    }
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedSfixed32 = function(field, value) {
+  if (value == null || !value.length) return;
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
+  this.encoder_.writeUnsignedVarint32(value.length * 4);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeInt32(value[i]);
   }
 };
 
 
 /**
  * Writes an array of numbers to the buffer as a packed sfixed64 field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
-jspb.BinaryWriter.prototype.writePackedSfixed64 =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null) {
-    this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
-    this.rawWriteUnsignedVarint32(value.length * 8);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteInt64(value[i]);
-    }
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedSfixed64 = function(field, value) {
+  if (value == null || !value.length) return;
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
+  this.encoder_.writeUnsignedVarint32(value.length * 8);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeInt64(value[i]);
   }
 };
 
 
 /**
  * Writes an array of numbers to the buffer as a packed float field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
-jspb.BinaryWriter.prototype.writePackedFloat =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
-    this.rawWriteUnsignedVarint32(value.length * 4);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteFloat(value[i]);
-    }
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedFloat = function(field, value) {
+  if (value == null || !value.length) return;
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
+  this.encoder_.writeUnsignedVarint32(value.length * 4);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeFloat(value[i]);
   }
 };
 
 
 /**
  * Writes an array of numbers to the buffer as a packed double field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
-jspb.BinaryWriter.prototype.writePackedDouble =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
-    this.rawWriteUnsignedVarint32(value.length * 8);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteDouble(value[i]);
-    }
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedDouble = function(field, value) {
+  if (value == null || !value.length) return;
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
+  this.encoder_.writeUnsignedVarint32(value.length * 8);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeDouble(value[i]);
   }
 };
 
 
 /**
  * Writes an array of booleans to the buffer as a packed bool field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<boolean>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
-jspb.BinaryWriter.prototype.writePackedBool =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
-    this.rawWriteUnsignedVarint32(value.length);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteBool(value[i]);
-    }
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedBool = function(field, value) {
+  if (value == null || !value.length) return;
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
+  this.encoder_.writeUnsignedVarint32(value.length);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeBool(value[i]);
   }
 };
 
 
 /**
  * Writes an array of enums to the buffer as a packed enum field.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<number>} value The array of ints to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
-jspb.BinaryWriter.prototype.writePackedEnum =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    var bookmark = this.beginDelimited_(field);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteEnum(value[i]);
-    }
-    this.endDelimited_(bookmark);
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedEnum = function(field, value) {
+  if (value == null || !value.length) return;
+  var bookmark = this.beginDelimited_(field);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeEnum(value[i]);
+  }
+  this.endDelimited_(bookmark);
+};
+
+
+/**
+ * Writes a 64-bit hash string field (8 characters @ 8 bits of data each) to
+ * the buffer.
+ * @param {number} field The field number.
+ * @param {?Array.<string>} value The array of hashes to write.
+ */
+jspb.BinaryWriter.prototype.writePackedFixedHash64 = function(field, value) {
+  if (value == null || !value.length) return;
+  this.writeFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
+  this.encoder_.writeUnsignedVarint32(value.length * 8);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeFixedHash64(value[i]);
   }
 };
 
@@ -2073,52 +1548,14 @@
 /**
  * Writes a 64-bit hash string field (8 characters @ 8 bits of data each) to
  * the buffer.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
  * @param {number} field The field number.
  * @param {?Array.<string>} value The array of hashes to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
  */
-jspb.BinaryWriter.prototype.writePackedFixedHash64 =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    this.rawWriteFieldHeader_(field, jspb.BinaryConstants.WireType.DELIMITED);
-    this.rawWriteUnsignedVarint32(value.length * 8);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteFixedHash64(value[i]);
-    }
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
+jspb.BinaryWriter.prototype.writePackedVarintHash64 = function(field, value) {
+  if (value == null || !value.length) return;
+  var bookmark = this.beginDelimited_(field);
+  for (var i = 0; i < value.length; i++) {
+    this.encoder_.writeVarintHash64(value[i]);
   }
-};
-
-
-/**
- * Writes a 64-bit hash string field (8 characters @ 8 bits of data each) to
- * the buffer.
- *
- * If 'value' is null, this method will try and copy the pre-serialized value
- * in 'opt_buffer' if present.
- *
- * @param {number} field The field number.
- * @param {?Array.<string>} value The array of hashes to write.
- * @param {?Uint8Array=} opt_buffer A buffer containing pre-packed values.
- * @param {?number=} opt_start The starting point in the above buffer.
- * @param {?number=} opt_end The ending point in the above buffer.
- */
-jspb.BinaryWriter.prototype.writePackedVarintHash64 =
-    function(field, value, opt_buffer, opt_start, opt_end) {
-  if (value != null && value.length) {
-    var bookmark = this.beginDelimited_(field);
-    for (var i = 0; i < value.length; i++) {
-      this.rawWriteVarintHash64(value[i]);
-    }
-    this.endDelimited_(bookmark);
-  } else if ((opt_buffer != null) && (opt_start != null) && (opt_end != null)) {
-    this.rawWriteByteRange(opt_buffer, opt_start, opt_end);
-  }
+  this.endDelimited_(bookmark);
 };
diff --git a/js/debug.js b/js/debug.js
index 4838911..3701a09 100644
--- a/js/debug.js
+++ b/js/debug.js
@@ -68,7 +68,7 @@
  * Recursively introspects a message and the values its getters return to
  * make a best effort in creating a human readable representation of the
  * message.
- * @param {*} thing A jspb.Message, Array or primitive type to dump.
+ * @param {?} thing A jspb.Message, Array or primitive type to dump.
  * @return {*}
  * @private
  */
diff --git a/js/debug_test.js b/js/debug_test.js
index d7bf376..01cbf89 100644
--- a/js/debug_test.js
+++ b/js/debug_test.js
@@ -41,6 +41,7 @@
 goog.require('proto.jspb.test.Simple1');
 
 
+
 describe('debugTest', function() {
   it('testSimple1', function() {
     if (COMPILED) {
diff --git a/js/message.js b/js/message.js
index cef9aef..813e6b8 100644
--- a/js/message.js
+++ b/js/message.js
@@ -39,8 +39,8 @@
 
 goog.require('goog.array');
 goog.require('goog.asserts');
+goog.require('goog.crypt.base64');
 goog.require('goog.json');
-goog.require('goog.object');
 
 // Not needed in compilation units that have no protos with xids.
 goog.forwardDeclare('xid.String');
@@ -120,6 +120,14 @@
 
 
 /**
+ * @return {boolean} Does this field represent a sub Message?
+ */
+jspb.ExtensionFieldInfo.prototype.isMessageType = function() {
+  return !!this.ctor;
+};
+
+
+/**
  * Base class for all JsPb messages.
  * @constructor
  * @struct
@@ -166,6 +174,14 @@
 
 
 /**
+ * Does this browser support Uint8Aray typed arrays?
+ * @type {boolean}
+ * @private
+ */
+jspb.Message.SUPPORTS_UINT8ARRAY_ = (typeof Uint8Array == 'function');
+
+
+/**
  * The internal data array.
  * @type {!Array}
  * @protected
@@ -211,6 +227,14 @@
 
 
 /**
+ * Repeated float or double fields which have been converted to include only
+ * numbers and not strings holding "NaN", "Infinity" and "-Infinity".
+ * @private {!Object<number,boolean>|undefined}
+ */
+jspb.Message.prototype.convertedFloatingPointFields_;
+
+
+/**
  * The xid of this proto type (The same for all instances of a proto). Provides
  * a way to identify a proto by stable obfuscated name.
  * @see {xid}.
@@ -284,6 +308,8 @@
   msg.arrayIndexOffset_ = messageId === 0 ? -1 : 0;
   msg.array = data;
   jspb.Message.materializeExtensionObject_(msg, suggestedPivot);
+  msg.convertedFloatingPointFields_ = {};
+
   if (repeatedFields) {
     for (var i = 0; i < repeatedFields.length; i++) {
       var fieldNumber = repeatedFields[i];
@@ -419,8 +445,9 @@
  * @param {!jspb.Message} proto The proto whose extensions to convert.
  * @param {!Object} obj The Soy object to add converted extension data to.
  * @param {!Object} extensions The proto class' registered extensions.
- * @param {function(jspb.ExtensionFieldInfo) : *} getExtensionFn The proto
- *     class' getExtension function. Passed for effective dead code removal.
+ * @param {function(this:?, jspb.ExtensionFieldInfo) : *} getExtensionFn
+ *     The proto class' getExtension function. Passed for effective dead code
+ *     removal.
  * @param {boolean=} opt_includeInstance Whether to include the JSPB instance
  *     for transitional soy proto support: http://goto/soy-param-migration
  */
@@ -472,7 +499,7 @@
     }
     var value = getExtensionFn.call(proto, fieldInfo);
     if (value) {
-      if (fieldInfo.ctor) {  // is this a message type?
+      if (fieldInfo.isMessageType()) {
         // If the message type of the extension was generated without binary
         // support, there may not be a binary message serializer function, and
         // we can't know when we codegen the extending message that the extended
@@ -517,8 +544,7 @@
   }
 
   var value;
-  if (fieldInfo.ctor) {
-    // Message type.
+  if (fieldInfo.isMessageType()) {
     value = new fieldInfo.ctor();
     fieldInfo.binaryReaderFn.call(
         reader, value, fieldInfo.binaryMessageDeserializeFn);
@@ -567,6 +593,130 @@
 
 
 /**
+ * Gets the value of an optional float or double field.
+ * @param {!jspb.Message} msg A jspb proto.
+ * @param {number} fieldNumber The field number.
+ * @return {?number|undefined} The field's value.
+ * @protected
+ */
+jspb.Message.getOptionalFloatingPointField = function(msg, fieldNumber) {
+  var value = jspb.Message.getField(msg, fieldNumber);
+  // Converts "NaN", "Infinity" and "-Infinity" to their corresponding numbers.
+  return value == null ? value : +value;
+};
+
+
+/**
+ * Gets the value of a repeated float or double field.
+ * @param {!jspb.Message} msg A jspb proto.
+ * @param {number} fieldNumber The field number.
+ * @return {!Array<number>} The field's value.
+ * @protected
+ */
+jspb.Message.getRepeatedFloatingPointField = function(msg, fieldNumber) {
+  var values = jspb.Message.getField(msg, fieldNumber);
+  if (!msg.convertedFloatingPointFields_) {
+    msg.convertedFloatingPointFields_ = {};
+  }
+  if (!msg.convertedFloatingPointFields_[fieldNumber]) {
+    for (var i = 0; i < values.length; i++) {
+      // Converts "NaN", "Infinity" and "-Infinity" to their corresponding
+      // numbers.
+      values[i] = +values[i];
+    }
+    msg.convertedFloatingPointFields_[fieldNumber] = true;
+  }
+  return /** @type {!Array<number>} */ (values);
+};
+
+
+/**
+ * Coerce a 'bytes' field to a base 64 string.
+ * @param {string|Uint8Array|null} value
+ * @return {?string} The field's coerced value.
+ */
+jspb.Message.bytesAsB64 = function(value) {
+  if (value == null || goog.isString(value)) {
+    return value;
+  }
+  if (jspb.Message.SUPPORTS_UINT8ARRAY_ && value instanceof Uint8Array) {
+    return goog.crypt.base64.encodeByteArray(value);
+  }
+  goog.asserts.fail('Cannot coerce to b64 string: ' + goog.typeOf(value));
+  return null;
+};
+
+
+/**
+ * Coerce a 'bytes' field to a Uint8Array byte buffer.
+ * Note that Uint8Array is not supported on IE versions before 10 nor on Opera
+ * Mini. @see http://caniuse.com/Uint8Array
+ * @param {string|Uint8Array|null} value
+ * @return {?Uint8Array} The field's coerced value.
+ */
+jspb.Message.bytesAsU8 = function(value) {
+  if (value == null || value instanceof Uint8Array) {
+    return value;
+  }
+  if (goog.isString(value)) {
+    return goog.crypt.base64.decodeStringToUint8Array(value);
+  }
+  goog.asserts.fail('Cannot coerce to Uint8Array: ' + goog.typeOf(value));
+  return null;
+};
+
+
+/**
+ * Coerce a repeated 'bytes' field to an array of base 64 strings.
+ * Note: the returned array should be treated as immutable.
+ * @param {!Array<string>|!Array<!Uint8Array>} value
+ * @return {!Array<string?>} The field's coerced value.
+ */
+jspb.Message.bytesListAsB64 = function(value) {
+  jspb.Message.assertConsistentTypes_(value);
+  if (!value.length || goog.isString(value[0])) {
+    return /** @type {!Array<string>} */ (value);
+  }
+  return goog.array.map(value, jspb.Message.bytesAsB64);
+};
+
+
+/**
+ * Coerce a repeated 'bytes' field to an array of Uint8Array byte buffers.
+ * Note: the returned array should be treated as immutable.
+ * Note that Uint8Array is not supported on IE versions before 10 nor on Opera
+ * Mini. @see http://caniuse.com/Uint8Array
+ * @param {!Array<string>|!Array<!Uint8Array>} value
+ * @return {!Array<Uint8Array?>} The field's coerced value.
+ */
+jspb.Message.bytesListAsU8 = function(value) {
+  jspb.Message.assertConsistentTypes_(value);
+  if (!value.length || value[0] instanceof Uint8Array) {
+    return /** @type {!Array<!Uint8Array>} */ (value);
+  }
+  return goog.array.map(value, jspb.Message.bytesAsU8);
+};
+
+
+/**
+ * Asserts that all elements of an array are of the same type.
+ * @param {Array?} array The array to test.
+ * @private
+ */
+jspb.Message.assertConsistentTypes_ = function(array) {
+  if (goog.DEBUG && array && array.length > 1) {
+    var expected = goog.typeOf(array[0]);
+    goog.array.forEach(array, function(e) {
+      if (goog.typeOf(e) != expected) {
+        goog.asserts.fail('Inconsistent type in JSPB repeated field array. ' +
+            'Got ' + goog.typeOf(e) + ' expected ' + expected);
+      }
+    });
+  }
+};
+
+
+/**
  * Gets the value of a non-extension primitive field, with proto3 (non-nullable
  * primitives) semantics. Returns `defaultValue` if the field is not otherwise
  * set.
@@ -841,7 +991,7 @@
   }
   var fieldNumber = fieldInfo.fieldIndex;
   if (fieldInfo.isRepeated) {
-    if (fieldInfo.ctor) {
+    if (fieldInfo.isMessageType()) {
       if (!this.wrappers_[fieldNumber]) {
         this.wrappers_[fieldNumber] =
             goog.array.map(this.extensionObject_[fieldNumber] || [],
@@ -854,7 +1004,7 @@
       return this.extensionObject_[fieldNumber];
     }
   } else {
-    if (fieldInfo.ctor) {
+    if (fieldInfo.isMessageType()) {
       if (!this.wrappers_[fieldNumber] && this.extensionObject_[fieldNumber]) {
         this.wrappers_[fieldNumber] = new fieldInfo.ctor(
             /** @type {Array|undefined} */ (
@@ -871,7 +1021,8 @@
 /**
  * Sets the value of the extension field in the extended object.
  * @param {jspb.ExtensionFieldInfo} fieldInfo Specifies the field to set.
- * @param {jspb.Message|string|number|boolean|Array} value The value to set.
+ * @param {jspb.Message|string|Uint8Array|number|boolean|Array?} value The value
+ *     to set.
  */
 jspb.Message.prototype.setExtension = function(fieldInfo, value) {
   if (!this.wrappers_) {
@@ -881,7 +1032,7 @@
   var fieldNumber = fieldInfo.fieldIndex;
   if (fieldInfo.isRepeated) {
     value = value || [];
-    if (fieldInfo.ctor) {
+    if (fieldInfo.isMessageType()) {
       this.wrappers_[fieldNumber] = value;
       this.extensionObject_[fieldNumber] = goog.array.map(
           /** @type {Array<jspb.Message>} */ (value), function(msg) {
@@ -891,7 +1042,7 @@
       this.extensionObject_[fieldNumber] = value;
     }
   } else {
-    if (fieldInfo.ctor) {
+    if (fieldInfo.isMessageType()) {
       this.wrappers_[fieldNumber] = value;
       this.extensionObject_[fieldNumber] = value ? value.toArray() : value;
     } else {
@@ -958,49 +1109,110 @@
 
 
 /**
+ * Compares two message extension fields recursively.
+ * @param {!Object} extension1 The first field.
+ * @param {!Object} extension2 The second field.
+ * @return {boolean} true if the extensions are null/undefined, or otherwise
+ *     equal.
+ */
+jspb.Message.compareExtensions = function(extension1, extension2) {
+  extension1 = extension1 || {};
+  extension2 = extension2 || {};
+
+  var keys = {};
+  for (var name in extension1) {
+    keys[name] = 0;
+  }
+  for (var name in extension2) {
+    keys[name] = 0;
+  }
+  for (name in keys) {
+    if (!jspb.Message.compareFields(extension1[name], extension2[name])) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
  * Compares two message fields recursively.
  * @param {*} field1 The first field.
  * @param {*} field2 The second field.
  * @return {boolean} true if the fields are null/undefined, or otherwise equal.
  */
 jspb.Message.compareFields = function(field1, field2) {
-  if (goog.isObject(field1) && goog.isObject(field2)) {
-    var keys = {}, name, extensionObject1, extensionObject2;
-    for (name in field1) {
-      field1.hasOwnProperty(name) && (keys[name] = 0);
+  // If the fields are trivially equal, they're equal.
+  if (field1 == field2) return true;
+
+  // If the fields aren't trivially equal and one of them isn't an object,
+  // they can't possibly be equal.
+  if (!goog.isObject(field1) || !goog.isObject(field2)) {
+    return false;
+  }
+
+  // We have two objects. If they're different types, they're not equal.
+  field1 = /** @type {!Object} */(field1);
+  field2 = /** @type {!Object} */(field2);
+  if (field1.constructor != field2.constructor) return false;
+
+  // If both are Uint8Arrays, compare them element-by-element.
+  if (jspb.Message.SUPPORTS_UINT8ARRAY_ && field1.constructor === Uint8Array) {
+    var bytes1 = /** @type {!Uint8Array} */(field1);
+    var bytes2 = /** @type {!Uint8Array} */(field2);
+    if (bytes1.length != bytes2.length) return false;
+    for (var i = 0; i < bytes1.length; i++) {
+      if (bytes1[i] != bytes2[i]) return false;
     }
-    for (name in field2) {
-      field2.hasOwnProperty(name) && (keys[name] = 0);
-    }
-    for (name in keys) {
-      var val1 = field1[name], val2 = field2[name];
-      if (goog.isObject(val1) && !goog.isArray(val1)) {
-        if (extensionObject1 !== undefined) {
-          throw new Error('invalid jspb state');
-        }
-        extensionObject1 = goog.object.isEmpty(val1) ? undefined : val1;
+    return true;
+  }
+
+  // If they're both Arrays, compare them element by element except for the
+  // optional extension objects at the end, which we compare separately.
+  if (field1.constructor === Array) {
+    var extension1 = undefined;
+    var extension2 = undefined;
+
+    var length = Math.max(field1.length, field2.length);
+    for (var i = 0; i < length; i++) {
+      var val1 = field1[i];
+      var val2 = field2[i];
+
+      if (val1 && (val1.constructor == Object)) {
+        goog.asserts.assert(extension1 === undefined);
+        goog.asserts.assert(i === field1.length - 1);
+        extension1 = val1;
         val1 = undefined;
       }
-      if (goog.isObject(val2) && !goog.isArray(val2)) {
-        if (extensionObject2 !== undefined) {
-          throw new Error('invalid jspb state');
-        }
-        extensionObject2 = goog.object.isEmpty(val2) ? undefined : val2;
+
+      if (val2 && (val2.constructor == Object)) {
+        goog.asserts.assert(extension2 === undefined);
+        goog.asserts.assert(i === field2.length - 1);
+        extension2 = val2;
         val2 = undefined;
       }
+
       if (!jspb.Message.compareFields(val1, val2)) {
         return false;
       }
     }
-    if (extensionObject1 || extensionObject2) {
-      return jspb.Message.compareFields(extensionObject1, extensionObject2);
+
+    if (extension1 || extension2) {
+      extension1 = extension1 || {};
+      extension2 = extension2 || {};
+      return jspb.Message.compareExtensions(extension1, extension2);
     }
+
     return true;
   }
-  // Primitive fields, null and undefined compare as equal.
-  // This also forces booleans and 0/1 to compare as equal to ensure
-  // compatibility with the jspb serializer.
-  return field1 == field2;
+
+  // If they're both plain Objects (i.e. extensions), compare them as
+  // extensions.
+  if (field1.constructor === Object) {
+    return jspb.Message.compareExtensions(field1, field2);
+  }
+
+  throw new Error('Invalid type in JSPB array');
 };
 
 
diff --git a/js/message_test.js b/js/message_test.js
index f572188..01add5f 100644
--- a/js/message_test.js
+++ b/js/message_test.js
@@ -53,6 +53,8 @@
 goog.require('proto.jspb.test.DefaultValues');
 goog.require('proto.jspb.test.Empty');
 goog.require('proto.jspb.test.EnumContainer');
+goog.require('proto.jspb.test.floatingMsgField');
+goog.require('proto.jspb.test.FloatingPointFields');
 goog.require('proto.jspb.test.floatingStrField');
 goog.require('proto.jspb.test.HasExtensions');
 goog.require('proto.jspb.test.IndirectExtension');
@@ -60,7 +62,6 @@
 goog.require('proto.jspb.test.OptionalFields');
 goog.require('proto.jspb.test.OuterEnum');
 goog.require('proto.jspb.test.OuterMessage.Complex');
-goog.require('proto.jspb.test.simple1');
 goog.require('proto.jspb.test.Simple1');
 goog.require('proto.jspb.test.Simple2');
 goog.require('proto.jspb.test.SpecialCases');
@@ -74,7 +75,7 @@
 // CommonJS-LoadFromFile: test2_pb proto.jspb.test
 goog.require('proto.jspb.test.ExtensionMessage');
 goog.require('proto.jspb.test.TestExtensionsMessage');
-goog.require('proto.jspb.test.floatingMsgField');
+
 
 
 
@@ -98,12 +99,6 @@
     assertEquals('some_bytes', data.getBytesField());
   });
 
-  it('testNestedMessage', function() {
-    var msg = new proto.jspb.test.OuterMessage.Complex();
-    msg.setInnerComplexField(5);
-    assertObjectEquals({innerComplexField: 5}, msg.toObject());
-  });
-
   it('testComplexConversion', function() {
     var data1 = ['a',,, [, 11], [[, 22], [, 33]],, ['s1', 's2'],, 1];
     var data2 = ['a',,, [, 11], [[, 22], [, 33]],, ['s1', 's2'],, 1];
@@ -160,6 +155,13 @@
 
   });
 
+  it('testNestedComplexMessage', function() {
+    // Instantiate the message and set a unique field, just to ensure that we
+    // are not getting jspb.test.Complex instead.
+    var msg = new proto.jspb.test.OuterMessage.Complex();
+    msg.setInnerComplexField(5);
+  });
+
   it('testSpecialCases', function() {
     // Note: Some property names are reserved in JavaScript.
     // These names are converted to the Js property named pb_<reserved_name>.
@@ -544,12 +546,6 @@
     extendable.setExtension(proto.jspb.test.IndirectExtension.str, null);
     assertNull(extendable.getExtension(proto.jspb.test.IndirectExtension.str));
 
-    // These assertions will only work properly in uncompiled mode.
-    // Extension fields defined on proto2 Descriptor messages are filtered out.
-
-    // TODO(haberman): codegen changes to properly ignore descriptor.proto
-    // extensions need to be merged from google3.
-    // assertUndefined(proto.jspb.test.IsExtension['simpleOption']);
 
     // Extension fields with jspb.ignore = true are ignored.
     assertUndefined(proto.jspb.test.IndirectExtension['ignored']);
@@ -907,7 +903,7 @@
         message.getDefaultOneofACase());
 
     message =
-        new proto.jspb.test.TestMessageWithOneof(new Array(9).concat(567,890));
+        new proto.jspb.test.TestMessageWithOneof(new Array(9).concat(567, 890));
     assertEquals(1234, message.getAone());
     assertEquals(890, message.getAtwo());
     assertEquals(
@@ -936,7 +932,7 @@
             message.getDefaultOneofBCase());
 
         message = new proto.jspb.test.TestMessageWithOneof(
-            new Array(11).concat(567,890));
+            new Array(11).concat(567, 890));
         assertUndefined(message.getBone());
         assertEquals(890, message.getBtwo());
         assertEquals(
@@ -989,4 +985,26 @@
     assertEquals('y', array[4]);
   });
 
+  it('testFloatingPointFieldsSupportNan', function() {
+    var assertNan = function(x) {
+      assertTrue('Expected ' + x + ' (' + goog.typeOf(x) + ') to be NaN.',
+          goog.isNumber(x) && isNaN(x));
+    };
+
+    var message = new proto.jspb.test.FloatingPointFields([
+      'NaN', 'NaN', ['NaN', 'NaN'], 'NaN',
+      'NaN', 'NaN', ['NaN', 'NaN'], 'NaN'
+    ]);
+    assertNan(message.getOptionalFloatField());
+    assertNan(message.getRequiredFloatField());
+    assertNan(message.getRepeatedFloatFieldList()[0]);
+    assertNan(message.getRepeatedFloatFieldList()[1]);
+    assertNan(message.getDefaultFloatField());
+    assertNan(message.getOptionalDoubleField());
+    assertNan(message.getRequiredDoubleField());
+    assertNan(message.getRepeatedDoubleFieldList()[0]);
+    assertNan(message.getRepeatedDoubleFieldList()[1]);
+    assertNan(message.getDefaultDoubleField());
+  });
+
 });
diff --git a/js/proto3_test.js b/js/proto3_test.js
index f886871..4dd7790 100644
--- a/js/proto3_test.js
+++ b/js/proto3_test.js
@@ -28,6 +28,7 @@
 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+goog.require('goog.crypt.base64');
 goog.require('goog.testing.asserts');
 
 // CommonJS-LoadFromFile: testbinary_pb proto.jspb.test
@@ -37,31 +38,30 @@
 goog.require('proto.jspb.test.Proto3Enum');
 goog.require('proto.jspb.test.TestProto3');
 
+
+var BYTES = new Uint8Array([1, 2, 8, 9]);
+var BYTES_B64 = goog.crypt.base64.encodeByteArray(BYTES);
+
+
 /**
- * Helper: compare a bytes field to a string with codepoints 0--255.
+ * Helper: compare a bytes field to an expected value
  * @param {Uint8Array|string} arr
- * @param {string} str
+ * @param {Uint8Array} expected
  * @return {boolean}
  */
-function bytesCompare(arr, str) {
-  if (arr.length != str.length) {
+function bytesCompare(arr, expected) {
+  if (goog.isString(arr)) {
+    arr = goog.crypt.base64.decodeStringToUint8Array(arr);
+  }
+  if (arr.length != expected.length) {
     return false;
   }
-  if (typeof arr == 'string') {
-    for (var i = 0; i < arr.length; i++) {
-      if (arr.charCodeAt(i) != str.charCodeAt(i)) {
-        return false;
-      }
+  for (var i = 0; i < arr.length; i++) {
+    if (arr[i] != expected[i]) {
+      return false;
     }
-    return true;
-  } else {
-    for (var i = 0; i < arr.length; i++) {
-      if (arr[i] != str.charCodeAt(i)) {
-        return false;
-      }
-    }
-    return true;
   }
+  return true;
 }
 
 
@@ -86,13 +86,17 @@
     assertEquals(msg.getOptionalDouble(), 0);
     assertEquals(msg.getOptionalString(), '');
 
-    // If/when we change bytes fields to return Uint8Array, we'll want to switch
-    // to this assertion instead:
-    //assertEquals(msg.getOptionalBytes() instanceof Uint8Array, true);
+    // TODO(b/26173701): when we change bytes fields default getter to return
+    // Uint8Array, we'll want to switch this assertion to match the u8 case.
     assertEquals(typeof msg.getOptionalBytes(), 'string');
-
+    assertEquals(msg.getOptionalBytes_asU8() instanceof Uint8Array, true);
+    assertEquals(typeof msg.getOptionalBytes_asB64(), 'string');
     assertEquals(msg.getOptionalBytes().length, 0);
-    assertEquals(msg.getOptionalForeignEnum(), proto.jspb.test.Proto3Enum.PROTO3_FOO);
+    assertEquals(msg.getOptionalBytes_asU8().length, 0);
+    assertEquals(msg.getOptionalBytes_asB64(), '');
+
+    assertEquals(msg.getOptionalForeignEnum(),
+                 proto.jspb.test.Proto3Enum.PROTO3_FOO);
     assertEquals(msg.getOptionalForeignMessage(), undefined);
     assertEquals(msg.getOptionalForeignMessage(), undefined);
 
@@ -136,7 +140,7 @@
     msg.setOptionalDouble(-1.5);
     msg.setOptionalBool(true);
     msg.setOptionalString('hello world');
-    msg.setOptionalBytes('bytes');
+    msg.setOptionalBytes(BYTES);
     var submsg = new proto.jspb.test.ForeignMessage();
     submsg.setC(16);
     msg.setOptionalForeignMessage(submsg);
@@ -156,7 +160,7 @@
     msg.setRepeatedDoubleList([-1.5]);
     msg.setRepeatedBoolList([true]);
     msg.setRepeatedStringList(['hello world']);
-    msg.setRepeatedBytesList(['bytes']);
+    msg.setRepeatedBytesList([BYTES]);
     submsg = new proto.jspb.test.ForeignMessage();
     submsg.setC(1000);
     msg.setRepeatedForeignMessageList([submsg]);
@@ -181,7 +185,7 @@
     assertEquals(msg.getOptionalDouble(), -1.5);
     assertEquals(msg.getOptionalBool(), true);
     assertEquals(msg.getOptionalString(), 'hello world');
-    assertEquals(true, bytesCompare(msg.getOptionalBytes(), 'bytes'));
+    assertEquals(true, bytesCompare(msg.getOptionalBytes(), BYTES));
     assertEquals(msg.getOptionalForeignMessage().getC(), 16);
     assertEquals(msg.getOptionalForeignEnum(),
         proto.jspb.test.Proto3Enum.PROTO3_BAR);
@@ -201,7 +205,7 @@
     assertElementsEquals(msg.getRepeatedBoolList(), [true]);
     assertElementsEquals(msg.getRepeatedStringList(), ['hello world']);
     assertEquals(msg.getRepeatedBytesList().length, 1);
-    assertEquals(true, bytesCompare(msg.getRepeatedBytesList()[0], 'bytes'));
+    assertEquals(true, bytesCompare(msg.getRepeatedBytesList()[0], BYTES));
     assertEquals(msg.getRepeatedForeignMessageList().length, 1);
     assertEquals(msg.getRepeatedForeignMessageList()[0].getC(), 1000);
     assertElementsEquals(msg.getRepeatedForeignEnumList(),
@@ -242,11 +246,12 @@
     assertEquals(msg.getOneofString(), 'hello');
     assertEquals(msg.getOneofBytes(), undefined);
 
-    msg.setOneofBytes('\u00FF\u00FF');
+    msg.setOneofBytes(goog.crypt.base64.encodeString('\u00FF\u00FF'));
     assertEquals(msg.getOneofUint32(), undefined);
     assertEquals(msg.getOneofForeignMessage(), undefined);
     assertEquals(msg.getOneofString(), undefined);
-    assertEquals(msg.getOneofBytes(), '\u00FF\u00FF');
+    assertEquals(msg.getOneofBytes_asB64(),
+        goog.crypt.base64.encodeString('\u00FF\u00FF'));
   });
 
 
@@ -267,7 +272,7 @@
     msg.setOptionalBool(false);
     msg.setOptionalString('hello world');
     msg.setOptionalString('');
-    msg.setOptionalBytes('\u00FF\u00FF');
+    msg.setOptionalBytes(goog.crypt.base64.encodeString('\u00FF\u00FF'));
     msg.setOptionalBytes('');
     msg.setOptionalForeignMessage(new proto.jspb.test.ForeignMessage());
     msg.setOptionalForeignMessage(null);
@@ -280,4 +285,30 @@
     var serialized = msg.serializeBinary();
     assertEquals(0, serialized.length);
   });
+
+  /**
+   * Test that base64 string and Uint8Array are interchangeable in bytes fields.
+   */
+  it('testBytesFieldsInterop', function() {
+    var msg = new proto.jspb.test.TestProto3();
+    // Set as a base64 string and check all the getters work.
+    msg.setOptionalBytes(BYTES_B64);
+    assertTrue(bytesCompare(msg.getOptionalBytes_asU8(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes_asB64(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes(), BYTES));
+
+    // Test binary serialize round trip doesn't break it.
+    msg = proto.jspb.test.TestProto3.deserializeBinary(msg.serializeBinary());
+    assertTrue(bytesCompare(msg.getOptionalBytes_asU8(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes_asB64(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes(), BYTES));
+
+    msg = new proto.jspb.test.TestProto3();
+    // Set as a Uint8Array and check all the getters work.
+    msg.setOptionalBytes(BYTES);
+    assertTrue(bytesCompare(msg.getOptionalBytes_asU8(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes_asB64(), BYTES));
+    assertTrue(bytesCompare(msg.getOptionalBytes(), BYTES));
+
+  });
 });
diff --git a/js/test.proto b/js/test.proto
index 14418ac..6b9dc89 100644
--- a/js/test.proto
+++ b/js/test.proto
@@ -145,6 +145,17 @@
   optional bytes bytes_field = 8 [default="moo"]; // Base64 encoding is "bW9v"
 }
 
+message FloatingPointFields {
+  optional float optional_float_field = 1;
+  required float required_float_field = 2;
+  repeated float repeated_float_field = 3;
+  optional float default_float_field = 4 [default = 2.0];
+  optional double optional_double_field = 5;
+  required double required_double_field = 6;
+  repeated double repeated_double_field = 7;
+  optional double default_double_field = 8 [default = 2.0];
+}
+
 message TestClone {
   optional string str = 1;
   optional Simple1 simple1 = 3;
@@ -216,3 +227,4 @@
     int32 btwo = 13 [default = 1234];
   }
 }
+