Skip to content

Commit a38ae86

Browse files
committed
Blog: ArC & Gizmo 2 & breaking changes
1 parent ca3a7c2 commit a38ae86

File tree

1 file changed

+251
-0
lines changed

1 file changed

+251
-0
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
---
2+
layout: post
3+
title: 'ArC & Gizmo 2 & breaking changes'
4+
date: 2025-10-27
5+
tags: announcement arc gizmo
6+
synopsis: 'ArC got rewritten from Gizmo 1 to Gizmo 2. What does that mean for you?'
7+
author: lthon
8+
---
9+
10+
ArC is Quarkus's implementation of CDI Lite.
11+
Gizmo is a simplified bytecode generation library.
12+
What do they have in common?
13+
14+
ArC has been using Gizmo 1 since approximately forever, but now that Gizmo 2 is shaping up, some Quarkus components have started migrating to it.
15+
I have started rewriting ArC to Gizmo 2 a few months ago, when we felt like Gizmo 2 starts looking reasonable and some real-world experience is needed.
16+
17+
This rewrite took several months, mostly because Gizmo 2 is a complete rewrite and rearchitecture of Gizmo 1 and ArC is a heavy user, but also because during the ArC rewrite, I found some Gizmo 2 issues and there were several back and forths.
18+
19+
To illustrate, I'll first go over the differences in Gizmo 1 and 2, and then detail how does that affect ArC users.
20+
21+
== Gizmo 2
22+
23+
First off, Gizmo 1 is based on ASM and Gizmo 2 is based on the ClassFile API (not the one present in the JDK since link:https://openjdk.org/jeps/484[version 24], but the link:https://github.com/dmlloyd/jdk-classfile-backport[fork] maintained by David Lloyd, which supports Java 17).
24+
The ClassFile API itself is very different to ASM, and since the ClassFile API structure guided the Gizmo 2 API structure, that is also very different.
25+
26+
To quickly compare, this is how you generate a "Hello, World!" program with Gizmo 1:
27+
28+
[source,java]
29+
----
30+
ClassOutput output = ...;
31+
try (ClassCreator creator = ClassCreator.builder()
32+
.classOutput(output)
33+
.className("com.example.Hello")
34+
.build()) {
35+
MethodCreator method = creator.getMethodCreator("main", void.class, String[].class)
36+
.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
37+
Gizmo.systemOutPrintln(method, method.load("Hello, World!"));
38+
method.returnVoid();
39+
}
40+
----
41+
42+
And this is how you generate the same program with Gizmo 2:
43+
44+
[source,java]
45+
----
46+
Gizmo gizmo = Gizmo.create(ClassOutput.fileWriter(Path.of("target")));
47+
gizmo.class_("com.example.Hello", cc -> {
48+
cc.defaultConstructor();
49+
50+
cc.staticMethod("main", mc -> {
51+
ParamVar args = mc.parameter("args", String[].class);
52+
mc.body(bc -> {
53+
bc.printf("Hello, World!%n", List.of());
54+
bc.return_();
55+
});
56+
});
57+
});
58+
----
59+
60+
There are obvious surface-level differences in the API structure, but there are also deeper differences.
61+
I'll mention one here just as an example: how Gizmo represents and maintains values has changed significantly.
62+
63+
Gizmo 1 has the venerable `ResultHandle` class, which is almost always a local variable (even though the API doesn't let you assign to it; you have to use `AssignableResultHandle` for that).
64+
This means you don't really have to care about order in which you produce values or about using them multiple times -- everything just works.
65+
There's obvious overhead though: for each use of the value, it needs to be loaded from the variable to the stack.
66+
67+
On the other hand, Gizmo 2 represents values as ``Expr``s, which are _not_ local variables:
68+
69+
[source,java]
70+
----
71+
Expr hello = bc.invokeVirtual(
72+
MethodDesc.of(String.class, "concat", String.class, String.class),
73+
Const.of("Hello"), Const.of(" World"));
74+
----
75+
76+
An `Expr` is a value that is, at the time of its creation, on top of the stack, nothing more.
77+
This means the order of producing values suddenly matters and they may not be reused!
78+
To create a local variable (`LocalVar`) out of an expression, you have to explicitly call a method:
79+
80+
[source,java]
81+
----
82+
LocalVar hello = bc.localVar("hello", bc.invokeVirtual(
83+
MethodDesc.of(String.class, "concat", String.class, String.class),
84+
Const.of("Hello"), Const.of(" World")));
85+
----
86+
87+
There's a lot more concepts not shown in these examples, which you can read about in the documentation.
88+
The Gizmo 1 documentation is available at https://github.com/quarkusio/gizmo/blob/1.x/USAGE.adoc, while the Gizmo 2 documentation (not yet complete) is available at https://github.com/quarkusio/gizmo/blob/main/MANUAL.adoc.
89+
90+
== ArC
91+
92+
Back to ArC.
93+
Today, all bytecode generation in ArC is based on Gizmo 2 (if you want the gory details, look at https://github.com/quarkusio/quarkus/pull/50708[this pull request]), and it's going to be released in Quarkus 3.30.
94+
95+
ArC has several public APIs that expose Gizmo types.
96+
This means that the rewrite to Gizmo 2 includes breaking changes.
97+
These breaking changes are unlikely to affect users -- in fact, the number of affected places in the Quarkus core repository is surprisingly small.
98+
However, in the interest of transparency, here's a full list of API breakages:
99+
100+
1. `BeanConfiguratorBase`: methods
101+
+
102+
[source,java]
103+
----
104+
THIS creator(Consumer<MethodCreator> methodCreatorConsumer)
105+
THIS destroyer(Consumer<MethodCreator> methodCreatorConsumer)
106+
THIS checkActive(Consumer<MethodCreator> methodCreatorConsumer)
107+
----
108+
+
109+
were changed to
110+
+
111+
[source,java]
112+
----
113+
THIS creator(Consumer<CreateGeneration> creatorConsumer)
114+
THIS destroyer(Consumer<DestroyGeneration> destroyerConsumer)
115+
THIS checkActive(Consumer<CheckActiveGeneration> checkActiveConsumer)
116+
----
117+
118+
2. `ObserverConfigurator`: method
119+
+
120+
[source,java]
121+
----
122+
ObserverConfigurator notify(Consumer<MethodCreator> notifyConsumer)
123+
----
124+
+
125+
was changed to
126+
+
127+
[source,java]
128+
----
129+
ObserverConfigurator notify(Consumer<NotifyGeneration> notifyConsumer)
130+
----
131+
132+
3. `ContextConfigurator`: method
133+
+
134+
[source,java]
135+
----
136+
ContextConfigurator creator(Function<MethodCreator, ResultHandle> creator)
137+
----
138+
+
139+
was changed to
140+
+
141+
[source,java]
142+
----
143+
ContextConfigurator creator(Function<CreateGeneration, Expr> creator)
144+
----
145+
146+
4. `BeanProcessor.Builder`: method
147+
+
148+
[source,java]
149+
----
150+
Builder addSuppressConditionGenerator(Function<BeanInfo, Consumer<BytecodeCreator>> generator)
151+
----
152+
+
153+
was changed to
154+
+
155+
[source,java]
156+
----
157+
Builder addSuppressConditionGenerator(Function<BeanInfo, Consumer<BlockCreator>> generator)
158+
----
159+
160+
Noone is expected to be affected by the last change, because that is in the ArC integration API, which should only be used by the Quarkus ArC extension.
161+
The other changes are in APIs that could legitimately be used:
162+
163+
- synthetic beans
164+
- synthetic observers
165+
- custom contexts
166+
167+
As you see, all these changes are similar.
168+
The Gizmo 1 variant takes a `Consumer<MethodCreator>` (or, in one case, a `Function<MethodCreator, ResultHandle>`).
169+
The `MethodCreator` must be used to create the bytecode of the corresponding method:
170+
171+
- `BeanConfiguratorBase.creator()`: create an instance of the synthetic bean
172+
- `BeanConfiguratorBase.destroyer()`: destroy an instance of the synthetic bean
173+
- `BeanConfiguratorBase.checkActive()`: check if the synthetic bean is currently active (niche use case, most likely unused outside of the core Quarkus repository)
174+
- `ObserverConfigurator.notify()`: notify the synthetic observer
175+
- `ContextConfigurator.creator()`: create a context object of the custom context
176+
177+
The Gizmo 2 variants no longer take a Gizmo object.
178+
Instead, they take an ArC interface that provides access to all the necessary Gizmo objects -- because more than 1 is necessary.
179+
180+
As mentioned above, most extensions should not be affected.
181+
This is because higher-level APIs exist that do not expose bytecode generation; either they use classes that implement interfaces, or they accept results of recorder methods.
182+
These higher-level APIs didn't change at all.
183+
However, using the lower-level APIs is still permitted, so let's take a look at how we'd migrate a simple synthetic bean creation function from Gizmo 1 to Gizmo 2.
184+
185+
Here's a simple synthetic bean registered using `SyntheticBeanBuildItem`:
186+
187+
[source,java]
188+
----
189+
SyntheticBeanBuildItem.configure(String.class)
190+
.scope(Singleton.class)
191+
.param("message", "Hello, World!")
192+
.creator(mc -> {
193+
ResultHandle params = mc.readInstanceField(
194+
FieldDescriptor.of(mc.getMethodDescriptor().getDeclaringClass(),
195+
"params", Map.class),
196+
mc.getThis());
197+
ResultHandle message = Gizmo.mapOperations(mc).on(params).get(mc.load("message"));
198+
ResultHandle instance = mc.invokeVirtualMethod(
199+
MethodDescriptor.ofMethod(String.class,
200+
"concat", String.class, String.class),
201+
mc.load("Message: "), message);
202+
mc.returnValue(instance);
203+
})
204+
.done();
205+
----
206+
207+
The `Consumer` here accepts a `MethodCreator` that provides direct access to its parameters as well as to the class, from which one can read the fields.
208+
209+
After the rewrite to Gizmo 2, the code looks like:
210+
211+
[source,java]
212+
----
213+
SyntheticBeanBuildItem.configure(String.class)
214+
.scope(Singleton.class)
215+
.param("message", "Hello, World!")
216+
.creator(cg -> {
217+
BlockCreator bc = cg.createMethod();
218+
219+
Var params = cg.paramsMap();
220+
Expr message = bc.withMap(params).get(Const.of("message"));
221+
Expr instance = bc.invokeVirtual(
222+
MethodDesc.of(String.class,
223+
"concat", String.class, String.class),
224+
Const.of("Message: "), message);
225+
bc.return_(instance);
226+
})
227+
.done();
228+
----
229+
230+
The `Consumer` accepts `CreateGeneration` that provides access to the `BlockCreator` to generate bytecode (`createMethod()`) and a number of necessary variables.
231+
In this example, we use the `paramsMap()` method to acccess the parameter map.
232+
233+
The other APIs have changed in the same manner: instead of `MethodCreator`, the `Consumer` accepts `*Generation` which provides access to the `BlockCreator` and the necessary variables.
234+
235+
One might ask: why does the new API provide access to a `BlockCreator` and not to a `MethodCreator`, which clearly still exists in Gizmo 2?
236+
And it would be a good question.
237+
The answer, as it turns out, is efficiency.
238+
The previous API that did provide access to a `MethodCreator` required generating a whole new method that would only host the user-generated code.
239+
The new API that _doesn't_ provide access to a `MethodCreator` allows embedding the user-generated code into a method that contains other, ArC-generated code.
240+
Thus, the number of methods in the generated classes is smaller and the generated code is more compact.
241+
242+
== Conclusion
243+
244+
Gizmo 2 is an evolution (some might say _revolution_) of Gizmo 1, the simplified bytecode generation library used by all of Quarkus.
245+
ArC is a heavy used of Gizmo and it just recently migrated to Gizmo 2.
246+
There are some breaking changes that might affect Quarkus extensions (it should be obvious, but I'll repeat: all these APIs are in the "build time" scope, so they are only usable by Quarkus extensions; there's no change that would affect Quarkus applications).
247+
248+
In this post, we reviewed the API breakages and showed a simple migration scenario.
249+
Hopefully, your extensions are not affected, because they use the higher-level APIs, but if they are, you'll need to migrate as well.
250+
Then, your extension will only be compatible with Quarkus 3.30 and above; it will stop working with previous versions.
251+
Plan accordingly.

0 commit comments

Comments
 (0)