From f7513b434bfb369d29a1432f704b0ae560f7de22 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Sat, 22 Aug 2026 09:30:43 +0100 Subject: [PATCH 1/4] use varhandles to to improve perf in Protobuf Parser and Generator --- .../protobuf/ProtobufGenerator.java | 46 ++++++++++ .../dataformat/protobuf/ProtobufParser.java | 38 ++++++++- .../protobuf/ProtobufVarHandleUtil.java | 84 +++++++++++++++++++ 3 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java index 951efdac3..7cbaa50fd 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java @@ -17,6 +17,28 @@ public class ProtobufGenerator extends GeneratorBase { + /** + * Whether VarHandles are usable on this runtime; probed once at class load. + * Being {@code static final} lets the branches in the {@code _writeIntXX()} + * helpers fold away at JIT time. + * + * @since 3.3 + */ + private static final boolean _VARHANDLE_AVAILABLE = _checkVarHandleAvailable(); + + private static boolean _checkVarHandleAvailable() { + // NOTE: this call is what first loads `ProtobufVarHandleUtil`, and that + // class names `VarHandle` in its field/method signatures. On a runtime + // without `java.lang.invoke.VarHandle` (some Android builds) loading it + // raises `NoClassDefFoundError` -- an Error, not an Exception -- so + // `Throwable` is what has to be caught here. + try { + return ProtobufVarHandleUtil.isAvailable(); + } catch (Throwable t) { + return false; + } + } + /* /********************************************************************** /* Constants @@ -1662,6 +1684,12 @@ private final void _writeInt32(int v) throws JacksonException _ensureRoom(9); // max tag 5 bytes int ptr = _writeTag(_currPtr); final byte[] buf = _currBuffer; + // protobuf fixed32 is little-endian + if (_VARHANDLE_AVAILABLE) { + ProtobufVarHandleUtil.setInt(buf, ptr, v); + _currPtr = ptr + 4; + return; + } buf[ptr++] = (byte) v; v >>= 8; buf[ptr++] = (byte) v; @@ -1677,6 +1705,12 @@ private final void _writeInt32NoTag(int v) throws JacksonException _ensureRoom(4); int ptr = _currPtr; final byte[] buf = _currBuffer; + // protobuf fixed32 is little-endian + if (_VARHANDLE_AVAILABLE) { + ProtobufVarHandleUtil.setInt(buf, ptr, v); + _currPtr = ptr + 4; + return; + } buf[ptr++] = (byte) v; v >>= 8; buf[ptr++] = (byte) v; @@ -1693,6 +1727,12 @@ private final void _writeInt64(long v64) throws JacksonException int ptr = _writeTag(_currPtr); final byte[] buf = _currBuffer; + // protobuf fixed64 is little-endian: low 32 bits first, then high + if (_VARHANDLE_AVAILABLE) { + ProtobufVarHandleUtil.setLong(buf, ptr, v64); + _currPtr = ptr + 8; + return; + } int v = (int) v64; buf[ptr++] = (byte) v; @@ -1722,6 +1762,12 @@ private final void _writeInt64NoTag(long v64) throws JacksonException int ptr = _currPtr; final byte[] buf = _currBuffer; + // protobuf fixed64 is little-endian: low 32 bits first, then high + if (_VARHANDLE_AVAILABLE) { + ProtobufVarHandleUtil.setLong(buf, ptr, v64); + _currPtr = ptr + 8; + return; + } int v = (int) v64; buf[ptr++] = (byte) v; diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java index 146823f60..1bc9ac4c7 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java @@ -16,6 +16,28 @@ public class ProtobufParser extends ParserMinimalBase { + /** + * Whether VarHandles are usable on this runtime; probed once at class load. + * Being {@code static final} lets the branches in {@link #_decode32Bits()} + * and {@link #_decode64Bits()} fold away at JIT time. + * + * @since 3.3 + */ + private static final boolean _VARHANDLE_AVAILABLE = _checkVarHandleAvailable(); + + private static boolean _checkVarHandleAvailable() { + // NOTE: this call is what first loads `ProtobufVarHandleUtil`, and that + // class names `VarHandle` in its field/method signatures. On a runtime + // without `java.lang.invoke.VarHandle` (some Android builds) loading it + // raises `NoClassDefFoundError` -- an Error, not an Exception -- so + // `Throwable` is what has to be caught here. + try { + return ProtobufVarHandleUtil.isAvailable(); + } catch (Throwable t) { + return false; + } + } + // State constants // State right after parser created; may start root Object @@ -2948,8 +2970,14 @@ protected final int _decode32Bits() throws JacksonException { return _slow32(); } final byte[] b = _inputBuffer; - int v = (b[ptr] & 0xFF) + ((b[ptr+1] & 0xFF) << 8) - + ((b[ptr+2] & 0xFF) << 16) + ((b[ptr+3] & 0xFF) << 24); + // protobuf fixed32 is little-endian + final int v; + if (_VARHANDLE_AVAILABLE) { + v = ProtobufVarHandleUtil.getInt(b, ptr); + } else { + v = (b[ptr] & 0xFF) + ((b[ptr+1] & 0xFF) << 8) + + ((b[ptr+2] & 0xFF) << 16) + ((b[ptr+3] & 0xFF) << 24); + } _inputPtr = ptr+4; return v; } @@ -2979,6 +3007,12 @@ protected final long _decode64Bits() throws JacksonException { return _slow64(); } final byte[] b = _inputBuffer; + // protobuf fixed64 is little-endian; `_long()` of the two 32-bit halves + // below is just that same little-endian 8-byte read spelled out + if (_VARHANDLE_AVAILABLE) { + _inputPtr = ptr+8; + return ProtobufVarHandleUtil.getLong(b, ptr); + } int i1 = (b[ptr++] & 0xFF) | ((b[ptr++] & 0xFF) << 8) | ((b[ptr++] & 0xFF) << 16) | (b[ptr++] << 24); int i2 = (b[ptr++] & 0xFF) | ((b[ptr++] & 0xFF) << 8) diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java new file mode 100644 index 000000000..6376f5ab9 --- /dev/null +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java @@ -0,0 +1,84 @@ +package tools.jackson.dataformat.protobuf; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteOrder; + +/** + * Utility class that provides {@link VarHandle} instances for efficient + * multi-byte primitive reads and writes on byte arrays. + *

+ * NOTE: handles here are LITTLE-endian, unlike the big-endian ones CBOR needs: + * protobuf encodes its {@code fixed32}/{@code fixed64} types (and hence + * {@code float}/{@code double}) as little-endian. + *

+ * Handles are resolved once at class initialization. On runtimes where + * {@code MethodHandles.byteArrayViewVarHandle()} is unsupported (for example + * some Android runtimes) they are left {@code null} and {@link #isAvailable()} + * returns {@code false}. Callers MUST check {@link #isAvailable()} first: the + * {@code getXxx()}/{@code setXxx()} methods dereference the handles + * unconditionally, and the byte-shifting fallback lives in the caller, not here. + *

+ * Callers must also invoke {@link #isAvailable()} from within a + * {@code try}/{@code catch (Throwable)} block: this class names {@link VarHandle} + * in its field and method signatures, so on a runtime lacking + * {@code java.lang.invoke.VarHandle} entirely it fails to link, raising + * {@link LinkageError} before any code here can run. + * + * @since 3.3 + */ +final class ProtobufVarHandleUtil +{ + /** + * VarHandle for reading/writing an {@code int} as 4 little-endian bytes. + * {@code null} if VarHandles are unavailable. + */ + static final VarHandle INT_LE; + + /** + * VarHandle for reading/writing a {@code long} as 8 little-endian bytes. + * {@code null} if VarHandles are unavailable. + */ + static final VarHandle LONG_LE; + + static { + VarHandle intLe = null; + VarHandle longLe = null; + try { + intLe = MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.LITTLE_ENDIAN); + longLe = MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.LITTLE_ENDIAN); + } catch (Throwable t) { + // VarHandles not available (e.g., Android): fall back to manual byte shifting + } + INT_LE = intLe; + // assigned last: non-null implies every handle above resolved too + LONG_LE = longLe; + } + + private ProtobufVarHandleUtil() { } + + static boolean isAvailable() { + return LONG_LE != null; + } + + // Helper methods that read/write primitives via the class's own VarHandle + // fields. Only called when the corresponding field is non-null, which implies + // VarHandle is available on this runtime. Caller MUST also have verified that + // the full 4/8 bytes are within bounds of the given array. + + static int getInt(byte[] array, int offset) { + return (int) INT_LE.get(array, offset); + } + + static long getLong(byte[] array, int offset) { + return (long) LONG_LE.get(array, offset); + } + + static void setInt(byte[] array, int offset, int value) { + INT_LE.set(array, offset, value); + } + + static void setLong(byte[] array, int offset, long value) { + LONG_LE.set(array, offset, value); + } +} From 02dea31d1d0033723cec785d23b002380faca8a2 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 6 Sep 2026 20:52:18 -0700 Subject: [PATCH 2/4] Add release notes --- release-notes/CREDITS | 3 +++ release-notes/VERSION | 3 +++ 2 files changed, 6 insertions(+) diff --git a/release-notes/CREDITS b/release-notes/CREDITS index be1ad13c1..6f5d21a88 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -73,3 +73,6 @@ PJ Fanning (@pjfanning) * Contributed #762: (avro) Use `VarHandle` for `float`/`double` reads in Avro parser (3.3.0) +* Contributed #763: (protobuf) Use `VarHandle` for multi-byte primitive reads + and writes in `ProtobufParser` / `ProtobufGenerator` + (3.3.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index d8b05c9f3..34c5dfd62 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -32,6 +32,9 @@ implementations) (fix by @cowtowncoder, w/ Claude code) #762: (avro) Use `VarHandle` for `float`/`double` reads in Avro parser (contributed by @pjfanning) +#763: (protobuf) Use `VarHandle` for multi-byte primitive reads and writes in + `ProtobufParser` / `ProtobufGenerator` + (contributed by @pjfanning) #767: (smile) Use more efficient `String` construction wrt "Compact Strings" for "short" ASCII text values of async parser (fix by @cowtowncoder, w/ Claude code) From d2fbb69d98ec7e660235c3a3a4d3b8a1bfe17810 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 6 Sep 2026 20:54:18 -0700 Subject: [PATCH 3/4] Minor renaming for consistency --- .../jackson/dataformat/protobuf/ProtobufGenerator.java | 8 ++++---- .../tools/jackson/dataformat/protobuf/ProtobufParser.java | 4 ++-- .../dataformat/protobuf/ProtobufVarHandleUtil.java | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java index 7cbaa50fd..fb350dfeb 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java @@ -1686,7 +1686,7 @@ private final void _writeInt32(int v) throws JacksonException final byte[] buf = _currBuffer; // protobuf fixed32 is little-endian if (_VARHANDLE_AVAILABLE) { - ProtobufVarHandleUtil.setInt(buf, ptr, v); + ProtobufVarHandleUtil.setIntLE(buf, ptr, v); _currPtr = ptr + 4; return; } @@ -1707,7 +1707,7 @@ private final void _writeInt32NoTag(int v) throws JacksonException final byte[] buf = _currBuffer; // protobuf fixed32 is little-endian if (_VARHANDLE_AVAILABLE) { - ProtobufVarHandleUtil.setInt(buf, ptr, v); + ProtobufVarHandleUtil.setIntLE(buf, ptr, v); _currPtr = ptr + 4; return; } @@ -1729,7 +1729,7 @@ private final void _writeInt64(long v64) throws JacksonException // protobuf fixed64 is little-endian: low 32 bits first, then high if (_VARHANDLE_AVAILABLE) { - ProtobufVarHandleUtil.setLong(buf, ptr, v64); + ProtobufVarHandleUtil.setLongLE(buf, ptr, v64); _currPtr = ptr + 8; return; } @@ -1764,7 +1764,7 @@ private final void _writeInt64NoTag(long v64) throws JacksonException // protobuf fixed64 is little-endian: low 32 bits first, then high if (_VARHANDLE_AVAILABLE) { - ProtobufVarHandleUtil.setLong(buf, ptr, v64); + ProtobufVarHandleUtil.setLongLE(buf, ptr, v64); _currPtr = ptr + 8; return; } diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java index 1bc9ac4c7..d71ffa19e 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java @@ -2973,7 +2973,7 @@ protected final int _decode32Bits() throws JacksonException { // protobuf fixed32 is little-endian final int v; if (_VARHANDLE_AVAILABLE) { - v = ProtobufVarHandleUtil.getInt(b, ptr); + v = ProtobufVarHandleUtil.getIntLE(b, ptr); } else { v = (b[ptr] & 0xFF) + ((b[ptr+1] & 0xFF) << 8) + ((b[ptr+2] & 0xFF) << 16) + ((b[ptr+3] & 0xFF) << 24); @@ -3011,7 +3011,7 @@ protected final long _decode64Bits() throws JacksonException { // below is just that same little-endian 8-byte read spelled out if (_VARHANDLE_AVAILABLE) { _inputPtr = ptr+8; - return ProtobufVarHandleUtil.getLong(b, ptr); + return ProtobufVarHandleUtil.getLongLE(b, ptr); } int i1 = (b[ptr++] & 0xFF) | ((b[ptr++] & 0xFF) << 8) | ((b[ptr++] & 0xFF) << 16) | (b[ptr++] << 24); diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java index 6376f5ab9..484c19362 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufVarHandleUtil.java @@ -66,19 +66,19 @@ static boolean isAvailable() { // VarHandle is available on this runtime. Caller MUST also have verified that // the full 4/8 bytes are within bounds of the given array. - static int getInt(byte[] array, int offset) { + static int getIntLE(byte[] array, int offset) { return (int) INT_LE.get(array, offset); } - static long getLong(byte[] array, int offset) { + static long getLongLE(byte[] array, int offset) { return (long) LONG_LE.get(array, offset); } - static void setInt(byte[] array, int offset, int value) { + static void setIntLE(byte[] array, int offset, int value) { INT_LE.set(array, offset, value); } - static void setLong(byte[] array, int offset, long value) { + static void setLongLE(byte[] array, int offset, long value) { LONG_LE.set(array, offset, value); } } From 9f9874a1df611bbcd1f9456b2f98417205327f1a Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 6 Sep 2026 21:01:35 -0700 Subject: [PATCH 4/4] Refactoring --- .../protobuf/ProtobufByteShiftUtil.java | 66 +++++++++ .../protobuf/ProtobufGenerator.java | 78 ++-------- .../dataformat/protobuf/ProtobufParser.java | 28 ++-- .../protobuf/VarHandleFallbackTest.java | 136 ++++++++++++++++++ 4 files changed, 223 insertions(+), 85 deletions(-) create mode 100644 protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufByteShiftUtil.java create mode 100644 protobuf/src/test/java/tools/jackson/dataformat/protobuf/VarHandleFallbackTest.java diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufByteShiftUtil.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufByteShiftUtil.java new file mode 100644 index 000000000..e2a2c313b --- /dev/null +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufByteShiftUtil.java @@ -0,0 +1,66 @@ +package tools.jackson.dataformat.protobuf; + +/** + * Byte-shifting fallback for reading and writing multi-byte primitives on byte + * arrays, used on runtimes where {@link ProtobufVarHandleUtil} is unusable. + *

+ * NOTE: accessors here are LITTLE-endian, unlike the big-endian ones CBOR needs: + * protobuf encodes its {@code fixed32}/{@code fixed64} types (and hence + * {@code float}/{@code double}) as little-endian. + *

+ * IMPORTANT: this class must NOT reference {@code java.lang.invoke.VarHandle}, + * directly or indirectly: it is the fallback for runtimes that lack that type, + * and naming it here would make this class fail to link on exactly those + * runtimes. Keeping the two implementations in separate classes is what makes + * the fallback path safe; see {@link ProtobufVarHandleUtil} for the full pattern. + * + * @since 3.3 + */ +final class ProtobufByteShiftUtil +{ + private ProtobufByteShiftUtil() { } + + /** + * Reads 4 bytes starting at given offset as a little-endian {@code int}. + * Caller MUST have verified that {@code offset+4} is within bounds of + * given array. + */ + static int getIntLE(byte[] buffer, int offset) { + return (buffer[offset] & 0xFF) + | ((buffer[offset+1] & 0xFF) << 8) + | ((buffer[offset+2] & 0xFF) << 16) + | ((buffer[offset+3] & 0xFF) << 24); + } + + /** + * Reads 8 bytes starting at given offset as a little-endian {@code long}. + * Caller MUST have verified that {@code offset+8} is within bounds of + * given array. + */ + static long getLongLE(byte[] buffer, int offset) { + // the two 32-bit halves combine to exactly a little-endian 8-byte read + final int i1 = getIntLE(buffer, offset); + final int i2 = getIntLE(buffer, offset+4); + return (((long) i1) & 0xFFFFFFFFL) | (((long) i2) << 32); + } + + /** + * Writes given {@code int} as 4 little-endian bytes at given offset; caller + * MUST have verified that {@code offset+4} is within bounds of given array. + */ + static void setIntLE(byte[] buffer, int offset, int value) { + buffer[offset] = (byte) value; + buffer[offset+1] = (byte) (value >> 8); + buffer[offset+2] = (byte) (value >> 16); + buffer[offset+3] = (byte) (value >> 24); + } + + /** + * Writes given {@code long} as 8 little-endian bytes at given offset; caller + * MUST have verified that {@code offset+8} is within bounds of given array. + */ + static void setLongLE(byte[] buffer, int offset, long value) { + setIntLE(buffer, offset, (int) value); + setIntLE(buffer, offset+4, (int) (value >> 32)); + } +} diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java index fb350dfeb..701a11cfd 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufGenerator.java @@ -1687,17 +1687,10 @@ private final void _writeInt32(int v) throws JacksonException // protobuf fixed32 is little-endian if (_VARHANDLE_AVAILABLE) { ProtobufVarHandleUtil.setIntLE(buf, ptr, v); - _currPtr = ptr + 4; - return; + } else { + ProtobufByteShiftUtil.setIntLE(buf, ptr, v); } - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - _currPtr = ptr; + _currPtr = ptr + 4; } private final void _writeInt32NoTag(int v) throws JacksonException @@ -1708,17 +1701,10 @@ private final void _writeInt32NoTag(int v) throws JacksonException // protobuf fixed32 is little-endian if (_VARHANDLE_AVAILABLE) { ProtobufVarHandleUtil.setIntLE(buf, ptr, v); - _currPtr = ptr + 4; - return; + } else { + ProtobufByteShiftUtil.setIntLE(buf, ptr, v); } - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - _currPtr = ptr; + _currPtr = ptr + 4; } private final void _writeInt64(long v64) throws JacksonException @@ -1730,30 +1716,10 @@ private final void _writeInt64(long v64) throws JacksonException // protobuf fixed64 is little-endian: low 32 bits first, then high if (_VARHANDLE_AVAILABLE) { ProtobufVarHandleUtil.setLongLE(buf, ptr, v64); - _currPtr = ptr + 8; - return; + } else { + ProtobufByteShiftUtil.setLongLE(buf, ptr, v64); } - int v = (int) v64; - - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - - v = (int) (v64 >> 32); - - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - - _currPtr = ptr; + _currPtr = ptr + 8; } private final void _writeInt64NoTag(long v64) throws JacksonException @@ -1765,30 +1731,10 @@ private final void _writeInt64NoTag(long v64) throws JacksonException // protobuf fixed64 is little-endian: low 32 bits first, then high if (_VARHANDLE_AVAILABLE) { ProtobufVarHandleUtil.setLongLE(buf, ptr, v64); - _currPtr = ptr + 8; - return; + } else { + ProtobufByteShiftUtil.setLongLE(buf, ptr, v64); } - int v = (int) v64; - - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - - v = (int) (v64 >> 32); - - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - v >>= 8; - buf[ptr++] = (byte) v; - - _currPtr = ptr; + _currPtr = ptr + 8; } /* diff --git a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java index d71ffa19e..fe0f6113b 100644 --- a/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java +++ b/protobuf/src/main/java/tools/jackson/dataformat/protobuf/ProtobufParser.java @@ -2971,13 +2971,9 @@ protected final int _decode32Bits() throws JacksonException { } final byte[] b = _inputBuffer; // protobuf fixed32 is little-endian - final int v; - if (_VARHANDLE_AVAILABLE) { - v = ProtobufVarHandleUtil.getIntLE(b, ptr); - } else { - v = (b[ptr] & 0xFF) + ((b[ptr+1] & 0xFF) << 8) - + ((b[ptr+2] & 0xFF) << 16) + ((b[ptr+3] & 0xFF) << 24); - } + final int v = _VARHANDLE_AVAILABLE + ? ProtobufVarHandleUtil.getIntLE(b, ptr) + : ProtobufByteShiftUtil.getIntLE(b, ptr); _inputPtr = ptr+4; return v; } @@ -3007,18 +3003,12 @@ protected final long _decode64Bits() throws JacksonException { return _slow64(); } final byte[] b = _inputBuffer; - // protobuf fixed64 is little-endian; `_long()` of the two 32-bit halves - // below is just that same little-endian 8-byte read spelled out - if (_VARHANDLE_AVAILABLE) { - _inputPtr = ptr+8; - return ProtobufVarHandleUtil.getLongLE(b, ptr); - } - int i1 = (b[ptr++] & 0xFF) | ((b[ptr++] & 0xFF) << 8) - | ((b[ptr++] & 0xFF) << 16) | (b[ptr++] << 24); - int i2 = (b[ptr++] & 0xFF) | ((b[ptr++] & 0xFF) << 8) - | ((b[ptr++] & 0xFF) << 16) | (b[ptr++] << 24); - _inputPtr = ptr; - return _long(i1, i2); + // protobuf fixed64 is little-endian + final long v = _VARHANDLE_AVAILABLE + ? ProtobufVarHandleUtil.getLongLE(b, ptr) + : ProtobufByteShiftUtil.getLongLE(b, ptr); + _inputPtr = ptr+8; + return v; } protected final long _slow64() throws JacksonException { diff --git a/protobuf/src/test/java/tools/jackson/dataformat/protobuf/VarHandleFallbackTest.java b/protobuf/src/test/java/tools/jackson/dataformat/protobuf/VarHandleFallbackTest.java new file mode 100644 index 000000000..929652fe8 --- /dev/null +++ b/protobuf/src/test/java/tools/jackson/dataformat/protobuf/VarHandleFallbackTest.java @@ -0,0 +1,136 @@ +package tools.jackson.dataformat.protobuf; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Random; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link ProtobufByteShiftUtil}, the byte-shifting fallback used on + * runtimes where {@link ProtobufVarHandleUtil} is unusable. + *

+ * Needed because {@code ProtobufParser._VARHANDLE_AVAILABLE} (and the generator's + * equivalent) is {@code static final} and true on every JDK we test on: the + * fallback branches are constant-folded away, so parsing and generation tests + * alone never execute them. Here the fallback is called directly and checked + * against {@link ByteBuffer} as an independent oracle, plus against the VarHandle + * implementation it stands in for. + */ +public class VarHandleFallbackTest extends ProtobufTestBase +{ + private final static int OFFSET = 3; // deliberately unaligned + + @Test + public void testGetIntLE() throws Exception + { + for (byte[] input : _inputs(4)) { + final int exp = ByteBuffer.wrap(input, OFFSET, 4) + .order(ByteOrder.LITTLE_ENDIAN).getInt(); + assertEquals(exp, ProtobufByteShiftUtil.getIntLE(input, OFFSET)); + if (ProtobufVarHandleUtil.isAvailable()) { + assertEquals(exp, ProtobufVarHandleUtil.getIntLE(input, OFFSET)); + } + } + } + + @Test + public void testGetLongLE() throws Exception + { + for (byte[] input : _inputs(8)) { + final long exp = ByteBuffer.wrap(input, OFFSET, 8) + .order(ByteOrder.LITTLE_ENDIAN).getLong(); + assertEquals(exp, ProtobufByteShiftUtil.getLongLE(input, OFFSET)); + if (ProtobufVarHandleUtil.isAvailable()) { + assertEquals(exp, ProtobufVarHandleUtil.getLongLE(input, OFFSET)); + } + } + } + + @Test + public void testSetIntLE() throws Exception + { + for (int value : _intValues()) { + byte[] exp = new byte[OFFSET+4]; + ByteBuffer.wrap(exp, OFFSET, 4).order(ByteOrder.LITTLE_ENDIAN).putInt(value); + + byte[] act = new byte[OFFSET+4]; + ProtobufByteShiftUtil.setIntLE(act, OFFSET, value); + assertArrayEquals(exp, act, "for value "+value); + + if (ProtobufVarHandleUtil.isAvailable()) { + byte[] viaHandle = new byte[OFFSET+4]; + ProtobufVarHandleUtil.setIntLE(viaHandle, OFFSET, value); + assertArrayEquals(exp, viaHandle, "for value "+value); + } + } + } + + @Test + public void testSetLongLE() throws Exception + { + for (long value : _longValues()) { + byte[] exp = new byte[OFFSET+8]; + ByteBuffer.wrap(exp, OFFSET, 8).order(ByteOrder.LITTLE_ENDIAN).putLong(value); + + byte[] act = new byte[OFFSET+8]; + ProtobufByteShiftUtil.setLongLE(act, OFFSET, value); + assertArrayEquals(exp, act, "for value "+value); + + if (ProtobufVarHandleUtil.isAvailable()) { + byte[] viaHandle = new byte[OFFSET+8]; + ProtobufVarHandleUtil.setLongLE(viaHandle, OFFSET, value); + assertArrayEquals(exp, viaHandle, "for value "+value); + } + } + } + + // // // Helper methods for building inputs: sign-bit and all-bits-set cases + // // // first, then pseudo-random ones (fixed seed, for reproducibility) + + private byte[][] _inputs(int length) { + final int size = OFFSET + length; + byte[][] result = new byte[3+100][]; + result[0] = new byte[size]; + result[1] = new byte[size]; + Arrays.fill(result[1], (byte) 0xFF); + result[2] = new byte[size]; + result[2][size-1] = (byte) 0x80; // high bit of most-significant (last) byte only + Random rnd = new Random(1234); + for (int i = 3; i < result.length; ++i) { + byte[] b = new byte[size]; + rnd.nextBytes(b); + result[i] = b; + } + return result; + } + + private int[] _intValues() { + int[] result = new int[4+100]; + result[0] = 0; + result[1] = -1; + result[2] = Integer.MIN_VALUE; + result[3] = Integer.MAX_VALUE; + Random rnd = new Random(5678); + for (int i = 4; i < result.length; ++i) { + result[i] = rnd.nextInt(); + } + return result; + } + + private long[] _longValues() { + long[] result = new long[4+100]; + result[0] = 0L; + result[1] = -1L; + result[2] = Long.MIN_VALUE; + result[3] = Long.MAX_VALUE; + Random rnd = new Random(9012); + for (int i = 4; i < result.length; ++i) { + result[i] = rnd.nextLong(); + } + return result; + } +}