Skip to content

Commit

Permalink
Add Gizmo.smartCast()
Browse files Browse the repository at this point in the history
  • Loading branch information
Ladicek committed Feb 5, 2024
1 parent 1141a04 commit 412c54b
Show file tree
Hide file tree
Showing 3 changed files with 320 additions and 1 deletion.
7 changes: 6 additions & 1 deletion USAGE.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,12 @@ There's also a variant that takes a statically known initial capacity: `Gizmo.ne

`Gizmo.newHashSet(BytecodeCreator target)` generates an invocation of `new HashSet()` into `target`.

`Gizmo.newHashMap(BytecodeCreator target)` generates an invocation of `new HashMap()` into target.
`Gizmo.newHashMap(BytecodeCreator target)` generates an invocation of `new HashMap()` into `target`.

`Gizmo.smartCast(BytecodeCreator bytecode, ResultHandle value, Class<?> castTarget)` generates a sequence of instructions to cast the `value` to the given `castTarget`, if such cast is possible using boxing, unboxing and primitive conversions, into `bytecode`.
Unlike `BytecodeCreator.checkCast()`, which only emits the `checkcast` instruction (possibly preceded by boxing), and `BytecodeCreator.convertPrimitive()`, which only performs primitive conversions (possibly preceded by unboxing), this method finds any cast that is possible.
For example, it allows casting `java.lang.Integer` to `java.lang.Long` by an unboxing conversion, followed by primitive conversion, followed by boxing conversion.
This method is _not_ equivalent to the cast conversion described by Java Language Specification; it is a superset.

=== StringBuilders

Expand Down
77 changes: 77 additions & 0 deletions src/main/java/io/quarkus/gizmo/Gizmo.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@

import org.objectweb.asm.Opcodes;

import static io.quarkus.gizmo.PrimitiveUtils.isPrimitiveDescriptor;
import static io.quarkus.gizmo.PrimitiveUtils.isWrapperDescriptor;
import static io.quarkus.gizmo.PrimitiveUtils.primitiveTypeFromDescriptor;
import static io.quarkus.gizmo.PrimitiveUtils.primitiveTypeFromWrapperDescriptor;

public final class Gizmo {

public static final int ASM_API_VERSION = Opcodes.ASM9;
Expand Down Expand Up @@ -443,6 +448,78 @@ public static ResultHandle newArrayList(BytecodeCreator target, int initialCapac
return target.newInstance(MethodDescriptor.ofConstructor(ArrayList.class, int.class), target.load(initialCapacity));
}

/**
* Casts given {@code value} to the given {@code castTarget}, using the boxing, unboxing and
* primitive conversions defined by the Java language specification.
* <p>
* Returns {@code value} directly when its static type is the same as {@code castTarget}.
* <p>
* Throws an exception if any of the arguments is {@code null}. Throws an exception when
* no combination of boxing, unboxing and primitive conversions exists from the type
* of {@code value} to the {@code castTarget}.
*
* @param bytecode where the bytecode instructions are emitted
* @param value the value to be casted
* @param castTarget the type to which the {@code value} should be casted
* @return the casted value
*/
public static ResultHandle smartCast(BytecodeCreator bytecode, ResultHandle value, Class<?> castTarget) {
return smartCast(bytecode, value, castTarget.getName());
}

/**
* Casts given {@code value} to the given {@code castTarget}, using the boxing, unboxing and
* primitive conversions defined by the Java language specification.
* <p>
* Returns {@code value} directly when its static type is the same as {@code castTarget}.
* <p>
* Throws an exception if any of the arguments is {@code null}. Throws an exception when
* no combination of boxing, unboxing and primitive conversions exists from the type
* of {@code value} to the {@code castTarget}.
*
* @param bytecode where the bytecode instructions are emitted
* @param value the value to be casted
* @param castTarget the type to which the {@code value} should be casted
* @return the casted value
*/
public static ResultHandle smartCast(BytecodeCreator bytecode, ResultHandle value, String castTarget) {
Objects.requireNonNull(bytecode);
Objects.requireNonNull(value);
Objects.requireNonNull(castTarget);

String sourceType = value.getType();
String targetType = DescriptorUtils.objectToDescriptor(castTarget);

boolean primitiveSource = isPrimitiveDescriptor(sourceType);
boolean primitiveTarget = isPrimitiveDescriptor(targetType);

if (sourceType.equals(targetType)) {
return value;
} else if (primitiveSource && primitiveTarget) {
return bytecode.convertPrimitive(value, primitiveTypeFromDescriptor(targetType));
} else if (!primitiveSource && !primitiveTarget) {
if (isWrapperDescriptor(sourceType) && isWrapperDescriptor(targetType)) {
// unboxes under the hood
value = bytecode.convertPrimitive(value, primitiveTypeFromWrapperDescriptor(targetType));
}
// boxes under the hood if required
return bytecode.checkCast(value, targetType);
} else if (primitiveSource /* && !primitiveTarget*/) {
if (!isWrapperDescriptor(targetType)) {
throw new IllegalArgumentException("Cannot convert primitive value to " + targetType + ": " + value);
}
ResultHandle tmp = bytecode.convertPrimitive(value, primitiveTypeFromWrapperDescriptor(targetType));
// boxes under the hood
return bytecode.checkCast(tmp, targetType);
} else /* if (!primitiveSource && primitiveTarget) */ {
if (!isWrapperDescriptor(sourceType)) {
throw new IllegalArgumentException("Cannot convert value to " + targetType + ": " + value);
}
// unboxes under the hood
return bytecode.convertPrimitive(value, primitiveTypeFromDescriptor(targetType));
}
}

// Implementation classes

/**
Expand Down
237 changes: 237 additions & 0 deletions src/test/java/io/quarkus/gizmo/SmartCastTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/*
* Copyright 2024 Red Hat, Inc.
*
* 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 io.quarkus.gizmo;

import org.junit.Test;

import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.function.Supplier;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class SmartCastTest {
public interface PrimitivesAndWrappers {
byte getbyte();
short getshort();
int getint();
long getlong();
float getfloat();
double getdouble();
char getchar();

Byte getbyteWrapper();
Short getshortWrapper();
Integer getintWrapper();
Long getlongWrapper();
Float getfloatWrapper();
Double getdoubleWrapper();
Character getcharWrapper();
}

// skipping `boolean` because there are no primitive conversions to/from boolean (except identity)
private static final List<Class<?>> PRIMITIVES = List.of(byte.class, short.class, int.class, long.class,
float.class, double.class, char.class);

@Test
public void fromByte() throws Exception {
test(m -> m.load((byte) 42));
}

@Test
public void fromShort() throws Exception {
test(m -> m.load((short) 42));
}

@Test
public void fromChar() throws Exception {
test(m -> m.load((char) 42));
}

@Test
public void fromInt() throws Exception {
test(m -> m.load(42));
}

@Test
public void fromLong() throws Exception {
test(m -> m.load(42L));
}

@Test
public void fromFloat() throws Exception {
test(m -> m.load(42.0F));
}

@Test
public void fromDouble() throws Exception {
test(m -> m.load(42.0));
}

@Test
public void fromByteWrapper() throws Exception {
test(m -> m.checkCast(m.load((byte) 42), Byte.class));
}

@Test
public void fromShortWrapper() throws Exception {
test(m -> m.checkCast(m.load((short) 42), Short.class));
}

@Test
public void fromCharWrapper() throws Exception {
test(m -> m.checkCast(m.load((char) 42), Character.class));
}

@Test
public void fromIntWrapper() throws Exception {
test(m -> m.checkCast(m.load(42), Integer.class));
}

@Test
public void fromLongWrapper() throws Exception {
test(m -> m.checkCast(m.load(42L), Long.class));
}

@Test
public void fromFloatWrapper() throws Exception {
test(m -> m.checkCast(m.load(42.0F), Float.class));
}

@Test
public void fromDoubleWrapper() throws Exception {
test(m -> m.checkCast(m.load(42.0), Double.class));
}

private void test(Function<MethodCreator, ResultHandle> load42) throws ReflectiveOperationException {
TestClassLoader cl = new TestClassLoader(getClass().getClassLoader());
try (ClassCreator creator = ClassCreator.builder()
.classOutput(cl)
.className("com.MyTest")
.interfaces(PrimitivesAndWrappers.class)
.build()) {
for (Class<?> primitive : PRIMITIVES) {
{
MethodCreator getPrimitive = creator.getMethodCreator("get" + primitive.getName(), primitive);
ResultHandle val = load42.apply(getPrimitive);
ResultHandle converted = Gizmo.smartCast(getPrimitive, val, primitive);
getPrimitive.returnValue(converted);
}

{
MethodCreator getPrimitiveWrapper = creator.getMethodCreator("get" + primitive.getName()
+ "Wrapper", PrimitiveUtils.WRAPPER_CLASS_BY_PRIMITIVE_KEYWORD.get(primitive.getName()));
ResultHandle val = load42.apply(getPrimitiveWrapper);
ResultHandle converted = Gizmo.smartCast(getPrimitiveWrapper, val, primitive);
getPrimitiveWrapper.returnValue(converted);
}
}
}
Class<?> clazz = cl.loadClass("com.MyTest");
PrimitivesAndWrappers primitivesAndWrappers = (PrimitivesAndWrappers) clazz.getDeclaredConstructor().newInstance();

assertEquals((byte) 42, primitivesAndWrappers.getbyte());
assertEquals((short) 42, primitivesAndWrappers.getshort());
assertEquals((char) 42, primitivesAndWrappers.getchar());
assertEquals(42, primitivesAndWrappers.getint());
assertEquals(42L, primitivesAndWrappers.getlong());
assertEquals(42.0F, primitivesAndWrappers.getfloat(), 0.0001F);
assertEquals(42.0, primitivesAndWrappers.getdouble(), 0.0001);

assertEquals(Byte.valueOf((byte) 42), primitivesAndWrappers.getbyteWrapper());
assertEquals(Short.valueOf((short) 42), primitivesAndWrappers.getshortWrapper());
assertEquals(Character.valueOf((char) 42), primitivesAndWrappers.getcharWrapper());
assertEquals(Integer.valueOf(42), primitivesAndWrappers.getintWrapper());
assertEquals(Long.valueOf(42L), primitivesAndWrappers.getlongWrapper());
assertEquals(Float.valueOf(42.0F), primitivesAndWrappers.getfloatWrapper(), 0.0001F);
assertEquals(Double.valueOf(42.0), primitivesAndWrappers.getdoubleWrapper(), 0.0001);
}

@Test
public void primitiveBooleanToPrimitiveBoolean() throws ReflectiveOperationException {
TestClassLoader cl = new TestClassLoader(getClass().getClassLoader());
try (ClassCreator creator = ClassCreator.builder()
.classOutput(cl)
.className("com.MyTest")
.interfaces(BooleanSupplier.class)
.build()) {
MethodCreator m = creator.getMethodCreator("getAsBoolean", boolean.class);
ResultHandle val = m.load(true);
ResultHandle converted = Gizmo.smartCast(m, val, boolean.class);
m.returnValue(converted);
}
Class<?> clazz = cl.loadClass("com.MyTest");
BooleanSupplier supplier = (BooleanSupplier) clazz.getDeclaredConstructor().newInstance();
assertTrue(supplier.getAsBoolean());
}

@Test
public void primitiveBooleanToBooleanWrapper() throws ReflectiveOperationException {
TestClassLoader cl = new TestClassLoader(getClass().getClassLoader());
try (ClassCreator creator = ClassCreator.builder()
.classOutput(cl)
.className("com.MyTest")
.interfaces(Supplier.class)
.build()) {
MethodCreator m = creator.getMethodCreator("get", Object.class);
ResultHandle val = m.load(true);
ResultHandle converted = Gizmo.smartCast(m, val, Boolean.class);
m.returnValue(converted);
}
Class<?> clazz = cl.loadClass("com.MyTest");
Supplier<?> supplier = (Supplier<?>) clazz.getDeclaredConstructor().newInstance();
assertTrue((Boolean) supplier.get());
}

@Test
public void booleanWrapperToPrimitiveBoolean() throws ReflectiveOperationException {
TestClassLoader cl = new TestClassLoader(getClass().getClassLoader());
try (ClassCreator creator = ClassCreator.builder()
.classOutput(cl)
.className("com.MyTest")
.interfaces(BooleanSupplier.class)
.build()) {
MethodCreator m = creator.getMethodCreator("getAsBoolean", boolean.class);
ResultHandle val = m.checkCast(m.load(true), Boolean.class);
ResultHandle converted = Gizmo.smartCast(m, val, boolean.class);
m.returnValue(converted);
}
Class<?> clazz = cl.loadClass("com.MyTest");
BooleanSupplier supplier = (BooleanSupplier) clazz.getDeclaredConstructor().newInstance();
assertTrue(supplier.getAsBoolean());
}

@Test
public void booleanWrapperToBooleanWrapper() throws ReflectiveOperationException {
TestClassLoader cl = new TestClassLoader(getClass().getClassLoader());
try (ClassCreator creator = ClassCreator.builder()
.classOutput(cl)
.className("com.MyTest")
.interfaces(Supplier.class)
.build()) {
MethodCreator m = creator.getMethodCreator("get", Object.class);
ResultHandle val = m.checkCast(m.load(true), Boolean.class);
ResultHandle converted = Gizmo.smartCast(m, val, Boolean.class);
m.returnValue(converted);
}
Class<?> clazz = cl.loadClass("com.MyTest");
Supplier<?> supplier = (Supplier<?>) clazz.getDeclaredConstructor().newInstance();
assertTrue((Boolean) supplier.get());
}
}

0 comments on commit 412c54b

Please sign in to comment.