Skip to content

Commit

Permalink
Java: Pulling in protobuf's faster UTF-8 encoder. (google#5035)
Browse files Browse the repository at this point in the history
* Pulling in protobuf's faster UTF-8 encoder.

* Remove Utf8 unsafe code.
  • Loading branch information
omalley authored and aardappel committed Dec 17, 2018
1 parent 9ad73bf commit cb99116
Show file tree
Hide file tree
Showing 6 changed files with 781 additions and 70 deletions.
66 changes: 33 additions & 33 deletions java/com/google/flatbuffers/FlatBufferBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.*;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.util.Arrays;
import java.nio.charset.Charset;

/// @file
/// @addtogroup flatbuffers_java_api
Expand All @@ -39,7 +35,6 @@ public class FlatBufferBuilder {
/// @cond FLATBUFFERS_INTERNAL
ByteBuffer bb; // Where we construct the FlatBuffer.
int space; // Remaining space in the ByteBuffer.
static final Charset utf8charset = Charset.forName("UTF-8"); // The UTF-8 character set used by FlatBuffers.
int minalign = 1; // Minimum alignment encountered so far.
int[] vtable = null; // The vtable for the current table.
int vtable_in_use = 0; // The amount of fields we're actually using.
Expand All @@ -50,9 +45,8 @@ public class FlatBufferBuilder {
int num_vtables = 0; // Number of entries in `vtables` in use.
int vector_num_elems = 0; // For the current vector being built.
boolean force_defaults = false; // False omits default values from the serialized data.
CharsetEncoder encoder = utf8charset.newEncoder();
ByteBuffer dst;
ByteBufferFactory bb_factory; // Factory for allocating the internal buffer
final Utf8 utf8; // UTF-8 encoder to use
/// @endcond

/**
Expand All @@ -62,10 +56,31 @@ public class FlatBufferBuilder {
* @param bb_factory The factory to be used for allocating the internal buffer
*/
public FlatBufferBuilder(int initial_size, ByteBufferFactory bb_factory) {
if (initial_size <= 0) initial_size = 1;
this(initial_size, bb_factory, null, Utf8.getDefault());
}

/**
* Start with a buffer of size `initial_size`, then grow as required.
*
* @param initial_size The initial size of the internal buffer to use.
* @param bb_factory The factory to be used for allocating the internal buffer
* @param existing_bb The byte buffer to reuse.
*/
public FlatBufferBuilder(int initial_size, ByteBufferFactory bb_factory,
ByteBuffer existing_bb, Utf8 utf8) {
if (initial_size <= 0) {
initial_size = 1;
}
space = initial_size;
this.bb_factory = bb_factory;
bb = bb_factory.newByteBuffer(initial_size);
if (existing_bb != null) {
bb = existing_bb;
bb.clear();
} else {
bb = bb_factory.newByteBuffer(initial_size);
}
bb.order(ByteOrder.LITTLE_ENDIAN);
this.utf8 = utf8;
}

/**
Expand All @@ -74,7 +89,7 @@ public FlatBufferBuilder(int initial_size, ByteBufferFactory bb_factory) {
* @param initial_size The initial size of the internal buffer to use.
*/
public FlatBufferBuilder(int initial_size) {
this(initial_size, new HeapByteBufferFactory());
this(initial_size, new HeapByteBufferFactory(), null, Utf8.getDefault());
}

/**
Expand All @@ -94,7 +109,7 @@ public FlatBufferBuilder() {
* the existing buffer needs to grow
*/
public FlatBufferBuilder(ByteBuffer existing_bb, ByteBufferFactory bb_factory) {
init(existing_bb, bb_factory);
this(existing_bb.capacity(), bb_factory, existing_bb, Utf8.getDefault());
}

/**
Expand All @@ -105,7 +120,7 @@ public FlatBufferBuilder(ByteBuffer existing_bb, ByteBufferFactory bb_factory) {
* @param existing_bb The byte buffer to reuse.
*/
public FlatBufferBuilder(ByteBuffer existing_bb) {
init(existing_bb, new HeapByteBufferFactory());
this(existing_bb, new HeapByteBufferFactory());
}

/**
Expand Down Expand Up @@ -503,27 +518,12 @@ public <T extends Table> int createSortedVectorOfTables(T obj, int[] offsets) {
* @return The offset in the buffer where the encoded string starts.
*/
public int createString(CharSequence s) {
int length = s.length();
int estimatedDstCapacity = (int) (length * encoder.maxBytesPerChar());
if (dst == null || dst.capacity() < estimatedDstCapacity) {
dst = ByteBuffer.allocate(Math.max(128, estimatedDstCapacity));
}

dst.clear();

CharBuffer src = s instanceof CharBuffer ? (CharBuffer) s :
CharBuffer.wrap(s);
CoderResult result = encoder.encode(src, dst, true);
if (result.isError()) {
try {
result.throwException();
} catch (CharacterCodingException x) {
throw new Error(x);
}
}

dst.flip();
return createString(dst);
int length = utf8.encodedLength(s);
addByte((byte)0);
startVector(1, length, 1);
bb.position(space -= length);
utf8.encodeUtf8(s, bb);
return endVector();
}

/**
Expand Down
38 changes: 2 additions & 36 deletions java/com/google/flatbuffers/Table.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,25 @@
import static com.google.flatbuffers.Constants.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;

/// @cond FLATBUFFERS_INTERNAL

/**
* All tables in the generated code derive from this class, and add their own accessors.
*/
public class Table {
private final static ThreadLocal<CharsetDecoder> UTF8_DECODER = new ThreadLocal<CharsetDecoder>() {
@Override
protected CharsetDecoder initialValue() {
return Charset.forName("UTF-8").newDecoder();
}
};
public final static ThreadLocal<Charset> UTF8_CHARSET = new ThreadLocal<Charset>() {
@Override
protected Charset initialValue() {
return Charset.forName("UTF-8");
}
};
private final static ThreadLocal<CharBuffer> CHAR_BUFFER = new ThreadLocal<CharBuffer>();
/** Used to hold the position of the `bb` buffer. */
protected int bb_pos;
/** The underlying ByteBuffer to hold the data of the Table. */
protected ByteBuffer bb;
Utf8 utf8 = Utf8.getDefault();

/**
* Get the underlying ByteBuffer.
Expand Down Expand Up @@ -98,34 +88,10 @@ protected static int __indirect(int offset, ByteBuffer bb) {
* @return Returns a `String` from the data stored inside the FlatBuffer at `offset`.
*/
protected String __string(int offset) {
CharsetDecoder decoder = UTF8_DECODER.get();
decoder.reset();

offset += bb.getInt(offset);
ByteBuffer src = bb.duplicate().order(ByteOrder.LITTLE_ENDIAN);
int length = src.getInt(offset);
src.position(offset + SIZEOF_INT);
src.limit(offset + SIZEOF_INT + length);

int required = (int)((float)length * decoder.maxCharsPerByte());
CharBuffer dst = CHAR_BUFFER.get();
if (dst == null || dst.capacity() < required) {
dst = CharBuffer.allocate(required);
CHAR_BUFFER.set(dst);
}

dst.clear();

try {
CoderResult cr = decoder.decode(src, dst, true);
if (!cr.isUnderflow()) {
cr.throwException();
}
} catch (CharacterCodingException x) {
throw new RuntimeException(x);
}

return dst.flip().toString();
return utf8.decodeUtf8(bb, offset + SIZEOF_INT, length);
}

/**
Expand Down
191 changes: 191 additions & 0 deletions java/com/google/flatbuffers/Utf8.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.flatbuffers;

import java.nio.ByteBuffer;

import static java.lang.Character.MIN_HIGH_SURROGATE;
import static java.lang.Character.MIN_LOW_SURROGATE;
import static java.lang.Character.MIN_SUPPLEMENTARY_CODE_POINT;

public abstract class Utf8 {

/**
* Returns the number of bytes in the UTF-8-encoded form of {@code sequence}. For a string,
* this method is equivalent to {@code string.getBytes(UTF_8).length}, but is more efficient in
* both time and space.
*
* @throws IllegalArgumentException if {@code sequence} contains ill-formed UTF-16 (unpaired
* surrogates)
*/
public abstract int encodedLength(CharSequence sequence);

/**
* Encodes the given characters to the target {@link ByteBuffer} using UTF-8 encoding.
*
* <p>Selects an optimal algorithm based on the type of {@link ByteBuffer} (i.e. heap or direct)
* and the capabilities of the platform.
*
* @param in the source string to be encoded
* @param out the target buffer to receive the encoded string.
*/
public abstract void encodeUtf8(CharSequence in, ByteBuffer out);

/**
* Decodes the given UTF-8 portion of the {@link ByteBuffer} into a {@link String}.
*
* @throws IllegalArgumentException if the input is not valid UTF-8.
*/
public abstract String decodeUtf8(ByteBuffer buffer, int offset, int length);

private static Utf8 DEFAULT;

/**
* Get the default UTF-8 processor.
* @return the default processor
*/
public static Utf8 getDefault() {
if (DEFAULT == null) {
DEFAULT = new Utf8Safe();
}
return DEFAULT;
}

/**
* Set the default instance of the UTF-8 processor.
* @param instance the new instance to use
*/
public static void setDefault(Utf8 instance) {
DEFAULT = instance;
}

/**
* Utility methods for decoding bytes into {@link String}. Callers are responsible for extracting
* bytes (possibly using Unsafe methods), and checking remaining bytes. All other UTF-8 validity
* checks and codepoint conversion happen in this class.
*/
static class DecodeUtil {

/**
* Returns whether this is a single-byte codepoint (i.e., ASCII) with the form '0XXXXXXX'.
*/
static boolean isOneByte(byte b) {
return b >= 0;
}

/**
* Returns whether this is a two-byte codepoint with the form '10XXXXXX'.
*/
static boolean isTwoBytes(byte b) {
return b < (byte) 0xE0;
}

/**
* Returns whether this is a three-byte codepoint with the form '110XXXXX'.
*/
static boolean isThreeBytes(byte b) {
return b < (byte) 0xF0;
}

static void handleOneByte(byte byte1, char[] resultArr, int resultPos) {
resultArr[resultPos] = (char) byte1;
}

static void handleTwoBytes(
byte byte1, byte byte2, char[] resultArr, int resultPos)
throws IllegalArgumentException {
// Simultaneously checks for illegal trailing-byte in leading position (<= '11000000') and
// overlong 2-byte, '11000001'.
if (byte1 < (byte) 0xC2
|| isNotTrailingByte(byte2)) {
throw new IllegalArgumentException("Invalid UTF-8");
}
resultArr[resultPos] = (char) (((byte1 & 0x1F) << 6) | trailingByteValue(byte2));
}

static void handleThreeBytes(
byte byte1, byte byte2, byte byte3, char[] resultArr, int resultPos)
throws IllegalArgumentException {
if (isNotTrailingByte(byte2)
// overlong? 5 most significant bits must not all be zero
|| (byte1 == (byte) 0xE0 && byte2 < (byte) 0xA0)
// check for illegal surrogate codepoints
|| (byte1 == (byte) 0xED && byte2 >= (byte) 0xA0)
|| isNotTrailingByte(byte3)) {
throw new IllegalArgumentException("Invalid UTF-8");
}
resultArr[resultPos] = (char)
(((byte1 & 0x0F) << 12) | (trailingByteValue(byte2) << 6) | trailingByteValue(byte3));
}

static void handleFourBytes(
byte byte1, byte byte2, byte byte3, byte byte4, char[] resultArr, int resultPos)
throws IllegalArgumentException{
if (isNotTrailingByte(byte2)
// Check that 1 <= plane <= 16. Tricky optimized form of:
// valid 4-byte leading byte?
// if (byte1 > (byte) 0xF4 ||
// overlong? 4 most significant bits must not all be zero
// byte1 == (byte) 0xF0 && byte2 < (byte) 0x90 ||
// codepoint larger than the highest code point (U+10FFFF)?
// byte1 == (byte) 0xF4 && byte2 > (byte) 0x8F)
|| (((byte1 << 28) + (byte2 - (byte) 0x90)) >> 30) != 0
|| isNotTrailingByte(byte3)
|| isNotTrailingByte(byte4)) {
throw new IllegalArgumentException("Invalid UTF-8");
}
int codepoint = ((byte1 & 0x07) << 18)
| (trailingByteValue(byte2) << 12)
| (trailingByteValue(byte3) << 6)
| trailingByteValue(byte4);
resultArr[resultPos] = DecodeUtil.highSurrogate(codepoint);
resultArr[resultPos + 1] = DecodeUtil.lowSurrogate(codepoint);
}

/**
* Returns whether the byte is not a valid continuation of the form '10XXXXXX'.
*/
private static boolean isNotTrailingByte(byte b) {
return b > (byte) 0xBF;
}

/**
* Returns the actual value of the trailing byte (removes the prefix '10') for composition.
*/
private static int trailingByteValue(byte b) {
return b & 0x3F;
}

private static char highSurrogate(int codePoint) {
return (char) ((MIN_HIGH_SURROGATE - (MIN_SUPPLEMENTARY_CODE_POINT >>> 10))
+ (codePoint >>> 10));
}

private static char lowSurrogate(int codePoint) {
return (char) (MIN_LOW_SURROGATE + (codePoint & 0x3ff));
}
}

// These UTF-8 handling methods are copied from Guava's Utf8Unsafe class with a modification to throw
// a protocol buffer local exception. This exception is then caught in CodedOutputStream so it can
// fallback to more lenient behavior.
static class UnpairedSurrogateException extends IllegalArgumentException {
UnpairedSurrogateException(int index, int length) {
super("Unpaired surrogate at index " + index + " of " + length);
}
}
}
Loading

0 comments on commit cb99116

Please sign in to comment.