Skip to content

Commit bf917fb

Browse files
committed
Blog: ArC migrates to Gizmo 2
1 parent ca3a7c2 commit bf917fb

File tree

1 file changed

+253
-0
lines changed

1 file changed

+253
-0
lines changed
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
---
2+
layout: post
3+
title: 'ArC migrates to Gizmo 2'
4+
date: 2025-10-31
5+
tags: 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+
Spoiler alert: there's no change that would affect Quarkus applications.
21+
All changes are in the APIs that are only exposed to extensions (at build time).
22+
23+
== Gizmo 1 vs Gizmo 2
24+
25+
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).
26+
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.
27+
28+
To quickly compare, this is how you generate a "Hello, World!" program with Gizmo 1:
29+
30+
[source,java]
31+
----
32+
ClassOutput output = ...;
33+
try (ClassCreator creator = ClassCreator.builder()
34+
.classOutput(output)
35+
.className("com.example.Hello")
36+
.build()) {
37+
MethodCreator method = creator.getMethodCreator("main", void.class, String[].class)
38+
.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
39+
Gizmo.systemOutPrintln(method, method.load("Hello, World!"));
40+
method.returnVoid();
41+
}
42+
----
43+
44+
And this is how you generate the same program with Gizmo 2:
45+
46+
[source,java]
47+
----
48+
Gizmo gizmo = Gizmo.create(ClassOutput.fileWriter(Path.of("target")));
49+
gizmo.class_("com.example.Hello", cc -> {
50+
cc.defaultConstructor();
51+
52+
cc.staticMethod("main", mc -> {
53+
ParamVar args = mc.parameter("args", String[].class);
54+
mc.body(bc -> {
55+
bc.printf("Hello, World!%n");
56+
bc.return_();
57+
});
58+
});
59+
});
60+
----
61+
62+
There are obvious surface-level differences in the API structure, but there are also deeper differences.
63+
I'll mention one here just as an example: the way Gizmo represents and maintains values has changed significantly.
64+
65+
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).
66+
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.
67+
There's obvious overhead though: for each use of the value, it needs to be loaded from the variable to the stack.
68+
69+
On the other hand, Gizmo 2 represents values as ``Expr``s, which are _not_ local variables:
70+
71+
[source,java]
72+
----
73+
Expr hello = bc.invokeVirtual(
74+
MethodDesc.of(String.class, "concat", String.class, String.class),
75+
Const.of("Hello"), Const.of(" World"));
76+
----
77+
78+
An `Expr` is a value that is, at the time of its creation, on top of the stack, nothing more.
79+
This means the order of producing values suddenly matters and they may not be reused!
80+
To create a local variable (`LocalVar`) out of an expression, you have to explicitly call a method:
81+
82+
[source,java]
83+
----
84+
LocalVar hello = bc.localVar("hello", bc.invokeVirtual(
85+
MethodDesc.of(String.class, "concat", String.class, String.class),
86+
Const.of("Hello"), Const.of(" World")));
87+
----
88+
89+
There's a lot more concepts not shown in these examples, which you can read about in the documentation.
90+
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.
91+
92+
== ArC
93+
94+
Back to ArC.
95+
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.
96+
97+
ArC has several public APIs that expose Gizmo types.
98+
This means that the rewrite to Gizmo 2 includes breaking changes.
99+
These breaking changes are unlikely to affect users -- in fact, the number of affected places in the Quarkus core repository is surprisingly small.
100+
However, in the interest of transparency, here's a full list of API breakages:
101+
102+
1. `BeanConfiguratorBase`: methods
103+
+
104+
[source,java]
105+
----
106+
THIS creator(Consumer<MethodCreator> methodCreatorConsumer)
107+
THIS destroyer(Consumer<MethodCreator> methodCreatorConsumer)
108+
THIS checkActive(Consumer<MethodCreator> methodCreatorConsumer)
109+
----
110+
+
111+
were changed to
112+
+
113+
[source,java]
114+
----
115+
THIS creator(Consumer<CreateGeneration> creatorConsumer)
116+
THIS destroyer(Consumer<DestroyGeneration> destroyerConsumer)
117+
THIS checkActive(Consumer<CheckActiveGeneration> checkActiveConsumer)
118+
----
119+
120+
2. `ObserverConfigurator`: method
121+
+
122+
[source,java]
123+
----
124+
ObserverConfigurator notify(Consumer<MethodCreator> notifyConsumer)
125+
----
126+
+
127+
was changed to
128+
+
129+
[source,java]
130+
----
131+
ObserverConfigurator notify(Consumer<NotifyGeneration> notifyConsumer)
132+
----
133+
134+
3. `ContextConfigurator`: method
135+
+
136+
[source,java]
137+
----
138+
ContextConfigurator creator(Function<MethodCreator, ResultHandle> creator)
139+
----
140+
+
141+
was changed to
142+
+
143+
[source,java]
144+
----
145+
ContextConfigurator creator(Function<CreateGeneration, Expr> creator)
146+
----
147+
148+
4. `BeanProcessor.Builder`: method
149+
+
150+
[source,java]
151+
----
152+
Builder addSuppressConditionGenerator(Function<BeanInfo, Consumer<BytecodeCreator>> generator)
153+
----
154+
+
155+
was changed to
156+
+
157+
[source,java]
158+
----
159+
Builder addSuppressConditionGenerator(Function<BeanInfo, Consumer<BlockCreator>> generator)
160+
----
161+
162+
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.
163+
The other changes are in APIs that could legitimately be used:
164+
165+
- synthetic beans
166+
- synthetic observers
167+
- custom contexts
168+
169+
As you see, all these changes are similar.
170+
The Gizmo 1 variant takes a `Consumer<MethodCreator>` (or, in one case, a `Function<MethodCreator, ResultHandle>`).
171+
The `MethodCreator` must be used to create the bytecode of the corresponding method:
172+
173+
- `BeanConfiguratorBase.creator()`: create an instance of the synthetic bean
174+
- `BeanConfiguratorBase.destroyer()`: destroy an instance of the synthetic bean
175+
- `BeanConfiguratorBase.checkActive()`: check if the synthetic bean is currently active (niche use case, most likely unused outside of the core Quarkus repository)
176+
- `ObserverConfigurator.notify()`: notify the synthetic observer
177+
- `ContextConfigurator.creator()`: create a context object of the custom context
178+
179+
The Gizmo 2 variants no longer take a Gizmo object.
180+
Instead, they take an ArC interface that provides access to all the necessary Gizmo objects -- because more than 1 is necessary.
181+
182+
As mentioned above, most extensions should not be affected.
183+
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.
184+
These higher-level APIs didn't change at all.
185+
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.
186+
187+
Here's a simple synthetic bean registered using `SyntheticBeanBuildItem`:
188+
189+
[source,java]
190+
----
191+
SyntheticBeanBuildItem.configure(String.class)
192+
.scope(Singleton.class)
193+
.param("message", "Hello, World!")
194+
.creator(mc -> {
195+
ResultHandle params = mc.readInstanceField(
196+
FieldDescriptor.of(mc.getMethodDescriptor().getDeclaringClass(),
197+
"params", Map.class),
198+
mc.getThis());
199+
ResultHandle message = Gizmo.mapOperations(mc).on(params).get(mc.load("message"));
200+
ResultHandle instance = mc.invokeVirtualMethod(
201+
MethodDescriptor.ofMethod(String.class,
202+
"concat", String.class, String.class),
203+
mc.load("Message: "), message);
204+
mc.returnValue(instance);
205+
})
206+
.done();
207+
----
208+
209+
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.
210+
211+
After the rewrite to Gizmo 2, the code looks like:
212+
213+
[source,java]
214+
----
215+
SyntheticBeanBuildItem.configure(String.class)
216+
.scope(Singleton.class)
217+
.param("message", "Hello, World!")
218+
.creator(cg -> {
219+
BlockCreator bc = cg.createMethod();
220+
221+
Var params = cg.paramsMap();
222+
Expr message = bc.withMap(params).get(Const.of("message"));
223+
Expr instance = bc.invokeVirtual(
224+
MethodDesc.of(String.class,
225+
"concat", String.class, String.class),
226+
Const.of("Message: "), message);
227+
bc.return_(instance);
228+
})
229+
.done();
230+
----
231+
232+
The `Consumer` accepts `CreateGeneration` that provides access to the `BlockCreator` to generate bytecode (`createMethod()`) and a number of necessary variables.
233+
In this example, we use the `paramsMap()` method to acccess the parameter map.
234+
235+
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.
236+
237+
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?
238+
And it would be a good question.
239+
The answer, as it turns out, is efficiency.
240+
The previous API that did provide access to a `MethodCreator` required generating a whole new method that would only host the user-generated code.
241+
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.
242+
Thus, the number of methods in the generated classes is smaller and the generated code is more compact.
243+
244+
== Conclusion
245+
246+
Gizmo 2 is an evolution (some might say _revolution_) of Gizmo 1, the simplified bytecode generation library used by all of Quarkus.
247+
ArC is a heavy user of Gizmo and it just recently migrated to Gizmo 2.
248+
There are some breaking changes that might affect Quarkus extensions (not applications).
249+
250+
In this post, we reviewed the API breakages and showed a simple migration scenario.
251+
Hopefully, your extensions are not affected, because they use the higher-level APIs, but if they are, you'll need to migrate as well.
252+
Then, your extension will only be compatible with Quarkus 3.30 and above; it will stop working with previous versions.
253+
Plan accordingly.

0 commit comments

Comments
 (0)