|
| 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