Skip to content
Draft
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
130 changes: 130 additions & 0 deletions base/src/main/java/org/eclipse/serializer/reflect/XReflect.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import static org.eclipse.serializer.util.X.notEmpty;
import static org.eclipse.serializer.util.X.notNull;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
Expand Down Expand Up @@ -56,6 +59,32 @@
*/
public final class XReflect
{
///////////////////////////////////////////////////////////////////////////
// constants //
//////////////

// null on a JVM without value class support (JEP 401). See #isValueClass.
private static final MethodHandle CLASS_IS_VALUE = resolveClassIsValue();

/* Any one of the JDK's cached-constant wrapper types being a value class is enough, and
* deliberately so - see #isValueClassEnabledRuntime. Must be declared after CLASS_IS_VALUE,
* static initializers running in declaration order.
*/
private static final boolean VALUE_CLASSES_ENABLED =
isValueClass(Boolean.class)
|| isValueClass(Byte.class)
|| isValueClass(Short.class)
|| isValueClass(Character.class)
|| isValueClass(Integer.class)
|| isValueClass(Long.class)
;



///////////////////////////////////////////////////////////////////////////
// static methods //
///////////////////

public static <T> T defaultInstantiate(final Class<T> type)
throws NoSuchMethodRuntimeException, InstantiationRuntimeException
{
Expand Down Expand Up @@ -273,6 +302,107 @@ public static boolean isDeclaredEnum(final Class<?> c)
{
return c != null && c.isEnum();
}

/**
* Tests whether the passed {@link Class} is a value class (JEP 401), i.e. a class whose instances
* have no identity.
* <p>
* Value instances must never be used where identity is required: they cannot be referenced by a
* {@link java.lang.ref.WeakReference} and cannot be synchronized on (both throw
* {@code java.lang.IdentityException}), while {@code ==} and
* {@link System#identityHashCode(Object)} compare their content instead of their identity.
* <p>
* The check is done reflectively via {@code Class#isValue}, since the compile baseline does not
* have that method. On a JVM without value class support, this method returns {@literal false}.
*
* @param c the {@link Class} to be tested, may be {@literal null}.
*
* @return whether the passed {@link Class} is a value class.
*/
public static boolean isValueClass(final Class<?> c)
{
/* A primitive and an interface both report themselves as value types, which they are in the type
* system's terms but not in the sense the callers ask about: neither declares fields or a
* constructor, and an interface-typed field holds a reference, never a value laid out in its owner.
*/
if(c == null || c.isPrimitive() || c.isInterface() || CLASS_IS_VALUE == null)
{
return false;
}

try
{
return (boolean)CLASS_IS_VALUE.invoke(c);
}
catch(final Throwable t)
{
// the handle is resolved from and invoked on java.lang.Class, so a failure is unrecoverable.
throw new Error("Failed to determine value class state of " + c.getName(), t);
}
}

/**
* Tests whether the passed instance is a value instance.
*
* @param instance the instance to be tested, may be {@literal null}.
*
* @return whether the passed instance is an instance of a value class.
*
* @see #isValueClass(Class)
*/
public static boolean isValueInstance(final Object instance)
{
return instance != null && isValueClass(instance.getClass());
}

/**
* Whether value classes are enabled in this runtime. Useful to skip logic that can only ever
* apply where an identity-less instance can exist at all.
* <p>
* Deliberately not the mere presence of {@code Class#isValue}: that method exists on every JDK
* since JEP 401 became a preview feature and reports {@literal false} for everything unless the
* JVM was started with {@code --enable-preview}. The wrapper types answer the actual state, since
* a user-defined value class is a preview class file that cannot be loaded without that flag.
* <p>
* <b>Any</b> of them suffices, not all, and the asymmetry is the point. The reader with something
* at stake is the constant registry: it holds its entries weakly, so registering one wrapper
* constant that is a value instance throws {@code IdentityException} and the storage does not
* open. Answering {@literal true} while they are all still identity classes costs nothing - their
* constant ids are then resolved arithmetically instead of from the registry, and over the cached
* range that yields the very same instances. So the expensive mistake is answering
* {@literal false} too readily, and one migrated wrapper is enough to rule it out.
* <p>
* For that reader this is exact. For the other kind - "can an identity-less instance exist at
* all", which is what a {@code GigaMap} segment asks before keeping a stored-state record - it
* stays a proxy: a JDK enabling value classes without migrating the wrappers would answer
* {@literal false} while a user's value class worked. That costs such an entity a fresh object id
* per store, nothing more.
*
* @return whether an instance without identity can exist in this JVM.
*
* @see #isValueClass(Class)
*/
public static boolean isValueClassEnabledRuntime()
{
return VALUE_CLASSES_ENABLED;
}

private static MethodHandle resolveClassIsValue()
{
try
{
return MethodHandles.publicLookup().findVirtual(
Class.class,
"isValue",
MethodType.methodType(boolean.class)
);
}
catch(final ReflectiveOperationException e)
{
// no value class support in this JVM.
return null;
}
}

public static boolean isSubEnum(final Class<?> c)
{
Expand Down
13 changes: 13 additions & 0 deletions base/src/main/java/org/eclipse/serializer/typing/ValueType.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,22 @@
* Value types are the only types where inherently implemented equals() and hashCode() are properly applicable.
* As Java is sadly missing a SELF typing, the untyped equals(Object obj) can't be defined more specific
* (like for example public boolean equals(SELF obj) or such).
* <p>
* Since JEP 401 the language says this directly: a {@code value class} has no identity, so {@code ==} is
* substitutability and the inherited {@code equals}/{@code hashCode} are state-based by construction. Such a
* type needs no marker, and marking it changes nothing.
* <p>
* What this interface is for is everything that cannot be one: a type that must be handled as a value but
* has to keep its identity - because it is not final, because something synchronizes on it or holds it
* weakly, or simply because it is compiled for a JDK without value classes. Implementing it is a statement
* about intended handling, not about the JVM's notion of a value.
* <p>
* Note that {@link XTypes#isValueType(Class)}, which is what reads this marker, does <b>not</b> recognise a
* JEP 401 value class - the two notions are separate. See that method for what the answer is used for.
*
* <p>
* Also see:
* @see XTypes#isValueType(Class)
* @see HashImmutable
* @see Immutable
* @see Stateless
Expand Down
29 changes: 29 additions & 0 deletions base/src/main/java/org/eclipse/serializer/typing/XTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,29 @@ public static boolean isLiteralType(final Class<?> c)
return c == String.class || c == char.class || c == Character.class;
}

/**
* Whether instances of the passed type are to be <i>handled</i> as values: by their state, rather than by
* which instance they are. The set is closed apart from the {@link ValueType} marker, which is the only
* way to add to it from outside this module.
* <p>
* This is a handling policy, not the JVM's notion of a value: a JEP 401 {@code value class} is
* deliberately <b>not</b> recognised here (see {@link org.eclipse.serializer.reflect.XReflect#isValueClass}
* for that question). For a value class whose fields are all primitives the distinction is invisible,
* because {@code ==} is then substitutability and {@code System.identityHashCode} is derived from state -
* so identity handling already behaves as value handling. It becomes visible for a value class holding a
* field of an identity type, where identity handling compares that field by reference while value
* handling would compare it by {@code equals}.
* <p>
* The reader with persistence-visible consequences is {@code BinaryHandlerGenericEnum}: a {@code final}
* enum field of a value type is treated as a value constant and is therefore <i>not</i> written back
* when the enum is loaded.
*
* @param c the type to be checked.
*
* @return whether instances of that type are to be handled as values.
*
* @see ValueType
*/
public static boolean isValueType(final Class<?> c)
{
// all value types, ordered in common use probability
Expand Down Expand Up @@ -228,9 +251,15 @@ public static boolean isBoolean(final Object o)
/**
* Checks if the type of the passed instance is an immutable special value type of the java language.
* This includes all primitive wrappers and {@link String}.
* <p>
* As with {@link #isValueType(Class)}, a JEP 401 value instance is not recognised here; see there for
* why that is invisible in most cases and where it is not. Note also that this variant does not cover
* {@link java.lang.reflect.Field}, which the {@link Class} variant does.
*
* @param o the instance to be checked
* @return {@code true} if the type of the passed instance is an immutable special value type.
*
* @see #isValueType(Class)
*/
public static boolean isValueType(final Object o)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package org.eclipse.serializer.persistence.binary.java.time;

/*-
* #%L
* Eclipse Serializer Persistence Binary
* %%
* Copyright (C) 2023 - 2026 MicroStream Software
* %%
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
* #L%
*/

import static org.eclipse.serializer.util.X.notNull;

import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;

import org.eclipse.serializer.memory.XMemory;
import org.eclipse.serializer.persistence.binary.types.AbstractBinaryHandlerCustomNonReferentialFixedLength;
import org.eclipse.serializer.persistence.binary.types.Binary;
import org.eclipse.serializer.persistence.types.PersistenceLoadHandler;
import org.eclipse.serializer.persistence.types.PersistenceStoreHandler;
import org.eclipse.serializer.reflect.XReflect;

/**
* Base for handlers of a {@code java.time} type that holds its state in a {@code long seconds} and an
* {@code int nanos} field, i.e. {@link java.time.Duration} and {@link java.time.Instant}. Both are
* value classes on a suitably recent JVM.
* <p>
* Where the handled type is a value class, its instances cannot be created empty and populated
* afterwards: the population write is not reliably visible on an identity-less instance. The instance
* is therefore built from its persisted values by {@link #createFromValues(long, int)}.
* <p>
* The instance is created complete either way, since a plugin reusing the value-type handlers (e.g.
* the REST viewer) relies on {@link #create} alone. Where the type is an ordinary class, an already
* registered instance is still populated in {@link #updateState}, preserving the update behavior the
* reflective handling had.
* <p>
* The persisted form is the two values under their field names qualified with the declaring type, so
* it is byte-identical to the one the reflective handling produced and existing data is unaffected.
*
* @param <T> the handled type.
*/
public abstract class AbstractBinaryHandlerSecondsNanos<T>
extends AbstractBinaryHandlerCustomNonReferentialFixedLength<T>
{
///////////////////////////////////////////////////////////////////////////
// constants //
//////////////

private static final long
BINARY_OFFSET_SECONDS = 0 ,
BINARY_OFFSET_NANOS = BINARY_OFFSET_SECONDS + Long.BYTES ,
BINARY_LENGTH = BINARY_OFFSET_NANOS + Integer.BYTES
;

private static final String
FIELD_NAME_SECONDS = "seconds",
FIELD_NAME_NANOS = "nanos"
;



///////////////////////////////////////////////////////////////////////////
// instance fields //
////////////////////

private final ToLongFunction<T> seconds;
private final ToIntFunction<T> nanos ;

// only needed where the type is an ordinary class and its instances are populated after creation.
private final long memoryOffsetSeconds;
private final long memoryOffsetNanos ;



///////////////////////////////////////////////////////////////////////////
// constructors //
/////////////////

protected AbstractBinaryHandlerSecondsNanos(
final Class<T> type ,
final ToLongFunction<T> seconds,
final ToIntFunction<T> nanos
)
{
super(
type,
CustomFields(
/* Qualified with the declaring type, so this description stays the one the reflective
* handling produced and data written before this handler existed still matches.
*/
CustomField(long.class, type.getName(), FIELD_NAME_SECONDS),
CustomField(int.class , type.getName(), FIELD_NAME_NANOS )
)
);

this.seconds = notNull(seconds);
this.nanos = notNull(nanos) ;

final boolean populated = !this.isValueClassType();
this.memoryOffsetSeconds = populated
? XMemory.objectFieldOffset(XReflect.getAnyField(type, FIELD_NAME_SECONDS))
: -1
;
this.memoryOffsetNanos = populated
? XMemory.objectFieldOffset(XReflect.getAnyField(type, FIELD_NAME_NANOS))
: -1
;
}



///////////////////////////////////////////////////////////////////////////
// declared methods //
/////////////////////

/**
* Builds an instance from its persisted values. The persisted nano value is always within
* {@code [0, 999_999_999]}, so a factory normalizing its arguments reproduces the state exactly.
*
* @param seconds the persisted second value.
* @param nanos the persisted nano-of-second value.
*
* @return the built instance.
*/
protected abstract T createFromValues(long seconds, int nanos);



///////////////////////////////////////////////////////////////////////////
// override methods //
/////////////////////

@Override
public void store(
final Binary data ,
final T instance,
final long objectId,
final PersistenceStoreHandler<Binary> handler
)
{
data.storeEntityHeader(BINARY_LENGTH, this.typeId(), objectId);
data.store_long(BINARY_OFFSET_SECONDS, this.seconds.applyAsLong(instance));
data.store_int (BINARY_OFFSET_NANOS , this.nanos.applyAsInt(instance) );
}

@Override
public T create(final Binary data, final PersistenceLoadHandler handler)
{
return this.createFromValues(
data.read_long(BINARY_OFFSET_SECONDS),
data.read_int (BINARY_OFFSET_NANOS)
);
}

@Override
public void updateState(final Binary data, final T instance, final PersistenceLoadHandler handler)
{
if(this.isValueClassType())
{
// already complete: it was built from its content rather than populated
return;
}

XMemory.set_long(instance, this.memoryOffsetSeconds, data.read_long(BINARY_OFFSET_SECONDS));
XMemory.set_int (instance, this.memoryOffsetNanos , data.read_int (BINARY_OFFSET_NANOS) );
}

}
Loading
Loading