picocli icon indicating copy to clipboard operation
picocli copied to clipboard

Generate dynamic proxies with code generation.

Open agentgt opened this issue 5 years ago • 10 comments

(this is not a bug but request for enhancement and some discussion).

The recent work on APT is awesome. I contemplated just manually doing our own command line parsing but with all this work on compile time generation I can avoid doing that in new projects! We are aiming to use less and less reflection in all of our projects (for both GraalVM and just in general lowering memory).

That being said while GraalVM does support dynamic proxies through registration as well as resources most of that can be totally eliminated with code generation.

How this is done is by using the ServiceLoader. The ServiceLoader is natively supported in GraalVM and does not require any configuration (resource registration). Thus concrete instances of interfaces can be generated and loaded with the ServiceLoader.

As a side note for discussion I noticed you use a lot of annotation data extraction as do all APT projects but that can done far easier with a not very well known project called Hickory: https://www.mvndoc.com/c/com.jolira/hickory/net/java/dev/hickory/prism/package-summary.html

Maven coordinates:

    <dependency>
      <groupId>com.jolira</groupId>
      <artifactId>hickory</artifactId>
      <version>1.0.0</version>
      <scope>provided</scope>
      <optional>true</optional>
    </dependency>

I believe MapStruct uses as do I for our internal projects. I highly recommend at least checking it out (and yes its does annotation processing and thus becomes a zero dependency).

agentgt avatar Jun 25 '19 13:06 agentgt

Thank you for the positive feedback and the link to the Hickory project!

Interesting idea to generate source or byte code for dynamic proxies and use the ServiceLoader to load the generated class.

I’m still a bit unclear on what the trade offs (benefits and drawbacks) would be of this approach, need to think about this more.

remkop avatar Jun 25 '19 14:06 remkop

I’m still a bit unclear on what the trade offs (benefits and drawbacks) would be of this approach, need to think about this more.

This is sort of likewise for me as well. It was more or less if you are going to generate code for the other stuff might as well generate the proxies however argument parsing of interface methods can be significantly painful (e.g. Micronaut has an enormous amount of code that does just that).

While I do know there is a speed improvement in regular Java VM (and most definitely in Android) from some rudimentary not very isolated tests however I don't know how much of a benefit it is in GraalVM. I'm not sure how much GraalVM does on optimizing proxies.

The real benefit I think is consistency and probably easier to understand/debug exceptions. Or at least that has been my experience but we don't use Picoli's proxy generation so I'm not sure.

agentgt avatar Jun 25 '19 14:06 agentgt

This is all good to know! My current focus is to get the 4.0-GA release out the door, after that I need to take a look at what the next milestone(s) should look like.

There is a plan to use the annotation processor to generate code so that a picocli-based application can be run without reflection, and perhaps this ticket can be part of that effort for the case of annotated interfaces.

#539 is still very early (analysis) stage though. I should probably take a look at how Micronaut does compile-time dependency injection. I gather that Micronaut generates additional classes (instead of the Grails approach of modifying user classes), but I am unclear on the details. If an app has annotated private fields, for example, then how can an additional generated class access (write values into) this private field without reflection? Anyway, lots of exciting learning ahead! :sweat_smile:

(Update: related: #1003)

remkop avatar Jun 25 '19 15:06 remkop

I should probably take a look at how Micronaut does compile-time dependency injection.

They generate a meta data class that basically has all the reflection information in it as statics. That generated class extends or implements an interface. I believe it's called BeanDefinition or something. Those classes are then loaded with the ServiceLoader.

I gather that Micronaut generates additional classes (instead of the Grails approach of modifying user classes), but I am unclear on the details

For Java they use ASM. Instead of generating code they generate directly to bytecode. Honestly I am impressed they were able to do this. I have to cheat and use ASMify or bytecode outline as I honestly can't remember Java byte code that well. Furthermore its easier to structure and plan in plain Java than going directly to bytecode.

There are some huge advantages to this approach in that Scala and Kotlin are less likely to have issues (in theory) as well as they claim it's easier to deal with than using templates or Java Poet since that route is filled with annoying pitfalls and source code that doesn't compile. I'm still in awe of it. Maybe ASM is easier to use the days.

I forgot to check how you are generating code but ASM is something to consider I guess if you have the patience.

... if an app has annotated private fields, for example, then how can an additional generated class access (write values into) this private field without reflection?

ASM also allows some level of private field access but to be honest I don't think its allowed in Micronaut like it is in Spring (ie @Autowire private Object field; ).

Any way for Groovy they use AST generator like newer Grails does I think.

agentgt avatar Jun 25 '19 16:06 agentgt

Thanks for that! :heart_eyes_cat: Impressive! :muscle: Did you learn all that from reading the Micronaut source code, or is there some online resource to look at? :eyes:

remkop avatar Jun 25 '19 16:06 remkop

I had to look at the code as noted here: https://github.com/micronaut-projects/micronaut-core/issues/445

@graemerocher Might add some documentation someday. The code is well written its just inherently complicated as there 3-4 APIs dealing with types and code generation at play.

It would be awesome someday if more of the Micronaut core code generation was separated as its own library particularly the bean introspection. This would allow more projects that use reflection for bean introspection (e.g. commons-beans) to stop doing this and not be coupled with micronaut core which is probably a more rapidly moving target. However I'm sure they have lots on their plate.

agentgt avatar Jun 25 '19 17:06 agentgt

I'm pretty sure that Dagger 2 also takes Micronaut's approach in that it generates code for dependency injection at compile time, so if investigating Micronaut leads to a dead end, then looking at Dagger 2 could be another option. :)

jbduncan avatar Jul 04 '19 07:07 jbduncan

That being said, IIRC, the way Dagger 2 generates code at compile time is by generating Java source files rather than bytecode. It's unclear to me at this stage which of the two approaches would be better for picocli. :thinking:

jbduncan avatar Jul 04 '19 07:07 jbduncan

With regards to generating bytecode vs source code, it's a bit too early to tell. I don't know much about generating bytecode, so there would be more of a learning curve for me than with generating source code, that is one factor. Generating Java source may be easier to follow and easier to debug, a bit less "magical" than generating bytecode. In the Micronaut ticket mentioned above, the Micronaut team give some insight in their rationale for generating bytecode. Some of these trade-offs may also hold for picocli, I'm not sure yet. I'll need to experiment a bit with both.

Related but slightly different problem: what I still haven't solved yet is this: how can generated code access annotated private fields. For example, take this command:

class App {
    @Option(name = "--option")
    private int option;
}

Let's say we create an annotation processor to generate a "companion" class, something like the below. The issue is how the generated class can get read/write access to the private fields of the command, without reflection.

// ICommandSpecFactory implementation (generated)
class App_Companion implements ICommandSpecFactory {
    public CommandSpec getCommandSpec(App userObject) {
        CommandSpec result = CommandSpec.wrapWithoutInspection(userObject);
        result.addOption(OptionSpec.builder("--option").type(int.class)
            .getter(new IGetter() {
                public Object get() {
                    return userObject.option; // compiler error: cannot access private field
                }
            })
            .setter(new ISetter() {
                public Object set(Object newValue) {
                    Object old = userObject.option; // compiler error: cannot access private field
                    userObject.option = (Integer) newValue; // same
                    return old;
                }
            })
            .build());
        return result;
    }
}

If we could modify the App class, we could generate an inner class, but annotation processors only allow generating additional files, not modifying existing ones.

At the moment the best I've been able to come up with is that any annotated elements cannot be private, but must be protected or package protected. Other ideas welcome!

remkop avatar Jul 04 '19 23:07 remkop

I believe MapStruct uses as do I for our internal projects. I highly recommend at least checking it out (and yes its does annotation processing and thus becomes a zero dependency).

Please check https://mapstruct.org/news/2020-02-03-announcing-gem-tools

rmuller avatar May 21 '20 07:05 rmuller