Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ PJ Fanning (@pjfanning)
* Contributed #752: (cbor) Use `VarHandle` for multi-byte primitive writes in
`CBORGenerator`
(3.3.0)
* Contributed #757: (smile) Use `VarHandle` for multi-byte primitive reads in
`SmileParser`
(3.3.0)
2 changes: 2 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ implementations)
(contributed by @pjfanning)
#752: (cbor) Use `VarHandle` for multi-byte primitive writes in `CBORGenerator`
(contributed by @pjfanning)
#757: (smile) Use `VarHandle` for multi-byte primitive reads in `SmileParser`
(contributed by @pjfanning)
#759: (smile) Use more efficient `String` construction wrt "Compact Strings"
for "long" text values
(fix by @cowtowncoder, w/ Claude code)
Expand Down
144 changes: 32 additions & 112 deletions smile/src/main/java/tools/jackson/dataformat/smile/SmileParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -979,18 +979,7 @@ private final int _nextNameOptimized(PropertyNameMatcher matcher, int len) throw
}
// First: maybe we already have this name decoded?
if (len < 5) {
int inPtr = _inputPtr;
final byte[] inBuf = _inputBuffer;
int q = inBuf[inPtr] & 0xFF;
if (len > 1) {
q = (q << 8) + (inBuf[++inPtr] & 0xFF);
if (len > 2) {
q = (q << 8) + (inBuf[++inPtr] & 0xFF);
if (len > 3) {
q = (q << 8) + (inBuf[++inPtr] & 0xFF);
}
}
}
int q = _decodePartialQuad(_inputBuffer, _inputPtr, len);
_quad1 = q;
return matcher.matchByQuad(q);
}
Expand All @@ -999,45 +988,21 @@ private final int _nextNameOptimized(PropertyNameMatcher matcher, int len) throw
int inPtr = _inputPtr;

// First quadbyte is easy
int q1 = (inBuf[inPtr++] & 0xFF);
q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF);
q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF);
q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF);
int q1 = _decodeQuad(inBuf, inPtr);
inPtr += 4;

if (len < 9) {
int q2 = (inBuf[inPtr++] & 0xFF);
int left = len - 5;
if (left > 0) {
q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF);
if (left > 1) {
q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF);
if (left > 2) {
q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF);
}
}
}
int q2 = _decodePartialQuad(inBuf, inPtr, len - 4);
_quad1 = q1;
_quad2 = q2;
return matcher.matchByQuad(q1, q2);
}

int q2 = (inBuf[inPtr++] & 0xFF);
q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF);
q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF);
q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF);
int q2 = _decodeQuad(inBuf, inPtr);
inPtr += 4;

if (len < 13) {
int q3 = (inBuf[inPtr++] & 0xFF);
int left = len - 9;
if (left > 0) {
q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF);
if (left > 1) {
q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF);
if (left > 2) {
q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF);
}
}
}
int q3 = _decodePartialQuad(inBuf, inPtr, len - 8);
_quad1 = q1;
_quad2 = q2;
_quad3 = q3;
Expand Down Expand Up @@ -1070,22 +1035,12 @@ private final int _nextNameFromSymbolsLong(PropertyNameMatcher matcher,

final byte[] inBuf = _inputBuffer;
do {
int q = (inBuf[inPtr++] & 0xFF);
q = (q << 8) | inBuf[inPtr++] & 0xFF;
q = (q << 8) | inBuf[inPtr++] & 0xFF;
q = (q << 8) | inBuf[inPtr++] & 0xFF;
_quadBuffer[offset++] = q;
_quadBuffer[offset++] = _decodeQuad(inBuf, inPtr);
inPtr += 4;
} while ((len -= 4) > 3);
// and then leftovers
if (len > 0) {
int q = inBuf[inPtr] & 0xFF;
if (len > 1) {
q = (q << 8) + (inBuf[++inPtr] & 0xFF);
if (len > 2) {
q = (q << 8) + (inBuf[++inPtr] & 0xFF);
}
}
_quadBuffer[offset++] = q;
_quadBuffer[offset++] = _decodePartialQuad(inBuf, inPtr, len);
}
return matcher.matchByQuad(_quadBuffer, offset);
}
Expand Down Expand Up @@ -2126,18 +2081,7 @@ private final String _findDecodedFromSymbols(final int len) throws JacksonExcept
{
// First: maybe we already have this name decoded?
if (len < 5) {
int inPtr = _inputPtr;
final byte[] inBuf = _inputBuffer;
int q = _padQuadForNulls(inBuf[inPtr]);
if (len > 1) {
q = (q << 8) + (inBuf[++inPtr] & 0xFF);
if (len > 2) {
q = (q << 8) + (inBuf[++inPtr] & 0xFF);
if (len > 3) {
q = (q << 8) + (inBuf[++inPtr] & 0xFF);
}
}
}
int q = _decodePartialQuadForNulls(_inputBuffer, _inputPtr, len);
_quad1 = q;
return _symbols.findName(q);
}
Expand All @@ -2146,45 +2090,21 @@ private final String _findDecodedFromSymbols(final int len) throws JacksonExcept
int inPtr = _inputPtr;

// First quadbyte is easy
int q1 = (inBuf[inPtr++] & 0xFF);
q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF);
q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF);
q1 = (q1 << 8) | (inBuf[inPtr++] & 0xFF);
int q1 = _decodeQuad(inBuf, inPtr);
inPtr += 4;

if (len < 9) {
int q2 = _padQuadForNulls(inBuf[inPtr++]);
int left = len - 5;
if (left > 0) {
q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF);
if (left > 1) {
q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF);
if (left > 2) {
q2 = (q2 << 8) + (inBuf[inPtr++] & 0xFF);
}
}
}
int q2 = _decodePartialQuadForNulls(inBuf, inPtr, len - 4);
_quad1 = q1;
_quad2 = q2;
return _symbols.findName(q1, q2);
}

int q2 = (inBuf[inPtr++] & 0xFF);
q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF);
q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF);
q2 = (q2 << 8) | (inBuf[inPtr++] & 0xFF);
int q2 = _decodeQuad(inBuf, inPtr);
inPtr += 4;

if (len < 13) {
int q3 = _padQuadForNulls(inBuf[inPtr++]);
int left = len - 9;
if (left > 0) {
q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF);
if (left > 1) {
q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF);
if (left > 2) {
q3 = (q3 << 8) + (inBuf[inPtr++] & 0xFF);
}
}
}
int q3 = _decodePartialQuadForNulls(inBuf, inPtr, len - 8);
_quad1 = q1;
_quad2 = q2;
_quad3 = q3;
Expand Down Expand Up @@ -2215,22 +2135,12 @@ private final String _findDecodedFixed12(int len, int q1, int q2) throws Jackson

final byte[] inBuf = _inputBuffer;
do {
int q = (inBuf[inPtr++] & 0xFF);
q = (q << 8) | inBuf[inPtr++] & 0xFF;
q = (q << 8) | inBuf[inPtr++] & 0xFF;
q = (q << 8) | inBuf[inPtr++] & 0xFF;
_quadBuffer[offset++] = q;
_quadBuffer[offset++] = _decodeQuad(inBuf, inPtr);
inPtr += 4;
} while ((len -= 4) > 3);
// and then leftovers
if (len > 0) {
int q = _padQuadForNulls(inBuf[inPtr]);
if (len > 1) {
q = (q << 8) + (inBuf[++inPtr] & 0xFF);
if (len > 2) {
q = (q << 8) + (inBuf[++inPtr] & 0xFF);
}
}
_quadBuffer[offset++] = q;
_quadBuffer[offset++] = _decodePartialQuadForNulls(inBuf, inPtr, len);
}
return _symbols.findName(_quadBuffer, offset);
}
Expand Down Expand Up @@ -2268,8 +2178,18 @@ private final static int _padLastQuad(int q, int bytes) {
return (bytes == 4) ? q : (q | (-1 << (bytes << 3)));
}

private final static int _padQuadForNulls(int firstByte) {
return (firstByte & 0xFF) | 0xFFFFFF00;
/**
* Variant of {@link #_decodePartialQuad} that pads the unused high bytes with
* 1s rather than 0s, which is what {@link ByteQuadsCanonicalizer} expects of a
* partial quad: without it a name ending in NULL bytes would collide with the
* shorter name that precedes those NULLs.
*
* @param len Number of bytes to decode; must be between 1 and 4
*
* @since 3.3
*/
private final static int _decodePartialQuadForNulls(byte[] buffer, int offset, int len) {
return _padLastQuad(_decodePartialQuad(buffer, offset, len), len);
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ public abstract class SmileParserBase extends ParserMinimalBase
protected final static JacksonFeatureSet<StreamReadCapability> SMILE_READ_CAPABILITIES
= DEFAULT_READ_CAPABILITIES.with(StreamReadCapability.EXACT_FLOATS);

/**
* Whether {@code VarHandle}-based array access is usable on this runtime;
* probed once at class load. See {@link #_decodeQuad}.
*
* @since 3.3
*/
private final static boolean _VARHANDLE_AVAILABLE = _checkVarHandleAvailable();

private static boolean _checkVarHandleAvailable() {
// NOTE: this call is what first loads `SmileVarHandleUtil`, 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 SmileVarHandleUtil.isAvailable();
Comment thread
cowtowncoder marked this conversation as resolved.
} catch (Throwable t) {
return false;
}
}

/*
/**********************************************************************
/* Config
Expand Down Expand Up @@ -720,6 +741,65 @@ protected void _reportMismatchedEndMarker(int actCh, char expCh) throws StreamRe
(char) actCh, expCh, ctxt.typeDesc(), ctxt.startLocation(_sourceReference())));
}

/**
* Helper method for decoding 4 bytes of an Object property name into the
* "quad" (big-endian {@code int}) form used by {@link ByteQuadsCanonicalizer}.
*<p>
* Uses a single unaligned load via {@code VarHandle} where supported,
* falling back to byte shifting otherwise. Since {@link #_VARHANDLE_AVAILABLE}
* is a {@code static final} the branch folds away at JIT time; and on the
* fallback path {@code SmileVarHandleUtil} is never resolved, which is what
* keeps this working on runtimes that lack {@code VarHandle} entirely.
*<p>
* Exists on the base class so that subclasses in the {@code async} package
* can share the same implementation; caller MUST have verified that 4 bytes
* are readable at given offset.
*
* @since 3.3
*/
protected final static int _decodeQuad(byte[] buffer, int offset) {
if (_VARHANDLE_AVAILABLE) {
return SmileVarHandleUtil.getIntBE(buffer, offset);
}
return ((buffer[offset] & 0xFF) << 24)
| ((buffer[offset+1] & 0xFF) << 16)
| ((buffer[offset+2] & 0xFF) << 8)
| (buffer[offset+3] & 0xFF);
}

/**
* Helper method for decoding the trailing 1 - 4 bytes of an Object property
* name into a right-aligned, zero-padded "quad": that is, produces the same
* value as accumulating {@code len} bytes with 8-bit shifts would.
*<p>
* Reads all 4 bytes with a single load where it can, which may read up to 3
* bytes past {@code offset+len}. Those bytes are shifted out and cannot
* affect the result, but the read still has to stay within the array: hence
* the length check, which also selects the byte-shifting path for a name
* that ends within 3 bytes of the end of the buffer.
*<p>
* Caller MUST have verified that {@code len} bytes are readable at given
* offset, and that {@code len} is between 1 and 4.
*
* @since 3.3
*/
protected final static int _decodePartialQuad(byte[] buffer, int offset, int len) {
if ((offset + 4) <= buffer.length) {
return _decodeQuad(buffer, offset) >>> ((4 - len) << 3);
}
int q = buffer[offset] & 0xFF;
if (len > 1) {
q = (q << 8) | (buffer[offset+1] & 0xFF);
if (len > 2) {
q = (q << 8) | (buffer[offset+2] & 0xFF);
if (len > 3) {
q = (q << 8) | (buffer[offset+3] & 0xFF);
}
}
}
return q;
}

/**
* Helper method used to encapsulate logic of including (or not) of
* "source reference" when constructing {@link TokenStreamLocation} instances.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package tools.jackson.dataformat.smile;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;

/**
* Utility class that provides {@link VarHandle}-based access for reading
* multi-byte primitives out of byte arrays.
*<p>
* IMPORTANT: this class references {@link VarHandle} in field and method
* signatures, so on a runtime that does not provide {@code java.lang.invoke.VarHandle}
* at all it will fail to <i>link</i>, before any code here gets a chance to run.
* Callers MUST therefore both:
*<ol>
* <li>load this class from within a {@code try}/{@code catch (Throwable)} block,
* so that {@link LinkageError} is caught, and</li>
* <li>keep the byte-shifting fallback in a class that does not reference
* {@link VarHandle}, so the fallback path never resolves this class.</li>
*</ol>
* {@code SmileParserBase._decodeQuad()} does both; see it for the pattern.
*
* @since 3.3
*/
final class SmileVarHandleUtil
{
/**
* VarHandle for reading 4 big-endian bytes as an {@code int}.
* {@code null} if {@code byteArrayViewVarHandle()} is unsupported.
*/
private static final VarHandle INT_BE;

static {
VarHandle intBe = null;
try {
intBe = MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.BIG_ENDIAN);
} catch (Throwable t) {
// Byte-array views not supported: caller falls back to byte shifting
}
INT_BE = intBe;
}

private SmileVarHandleUtil() { }

/**
* @return {@code true} if {@link #getIntBE} may be called; if {@code false},
* caller MUST use its own byte-shifting fallback
*/
static boolean isAvailable() {
return INT_BE != null;
}

/**
* Reads 4 bytes starting at given offset as a big-endian {@code int}.
*<p>
* Only to be called if {@link #isAvailable()} returned {@code true}: the
* handle is dereferenced unconditionally, and the fallback lives in the
* caller, not here.
* Caller MUST also have verified that {@code offset+4} is within bounds of
* given array.
*/
static int getIntBE(byte[] buffer, int offset) {
return (int) INT_BE.get(buffer, offset);
}
}
Loading