Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pre-compile FXML to avoid reflection during runtime #23

Open
hallvard opened this issue Feb 6, 2024 · 13 comments
Open

Pre-compile FXML to avoid reflection during runtime #23

hallvard opened this issue Feb 6, 2024 · 13 comments
Labels
enhancement New feature or request

Comments

@hallvard
Copy link

hallvard commented Feb 6, 2024

It would be beneficial to move FXML parsing to build time, not just for speed, but particularly since it uses reflection. I've started a small project for generating Java source from FXML, so you instead of using an FXMLLoader can call a method on a pre-generated Java class.

The question is what is needed for it to work well with quarkus, e.g. should there be a drop-in replacement for FXMLLoader that uses the pre-generated class instead? We could use an annotation on an interface and generate an implementation that will be injected and that builds the corresponding GUI? E.g. something along the following:

public interface MyFxml {

   @FxmlLoader("main-view.fxml")
   public Pane buildMainView();

   @FxmlLoader("settings-view.fxml")
   public Pane buildSettingsView();

   @FxmlBuilder("""
      <Label text="$aLabel"/>
   """)
   public Label createALabel(String aLabel);
}

...

@Inject
MyFxml myFxml;

Another thing to consider is FXML string templates (see prototype here: https://github.com/hallvard/fxml-template-processor).

I don't know much about Quarkus' annotation processing and build magic, so would like to open up a discussion about this in this issue.

@CodeSimcoe CodeSimcoe self-assigned this Feb 6, 2024
@CodeSimcoe CodeSimcoe added the enhancement New feature or request label Feb 6, 2024
@CodeSimcoe CodeSimcoe removed their assignment Feb 6, 2024
@CodeSimcoe
Copy link
Contributor

CodeSimcoe commented Feb 6, 2024

Very interesting, but i doubt it is easily feasible.
To load an FXML to get a Node, you'll need a running FX context which is usually not what you have at build time.
You also need to manage dynamic instantation at runtime (like in the sample/using-rest).

Anyway you got my curiosity and I'll have a look

@hallvard
Copy link
Author

hallvard commented Feb 6, 2024

I'm not planning to create the Node structure statically, but generate a class that is instantiated at runtime with a method that does the actual building of the JavaFX gui. The method contains all the Pane pane = new Pane(), Label label = new Label(), pane.getChildren().add(label) statements that correspond to what the FXMLLoader usually does. You save the parsing of the FXML and most importantly, the runtime reflection.

@CodeSimcoe CodeSimcoe assigned CodeSimcoe and unassigned CodeSimcoe Feb 7, 2024
@CodeSimcoe
Copy link
Contributor

CodeSimcoe commented Feb 7, 2024

The problem I see is that the single entry point for this is FXMLLoader::load and it does everything :

  • Parse the XML
  • Instantiate the controller and perform injection
  • Build UI component hierarchy

@hallvard
Copy link
Author

hallvard commented Feb 7, 2024

I will not use FXMLLoader directly (due to its use of reflection), but must ensure that the behavior of the parser and generated class is the same. So far I have a fairly good parser (using StAX) that builds a logical FXML document model, and a (so far) limited code generator, that handles the basic building of component hierarchy and setting of properties:

        List<Statement> javaCode = FxmlTranslator.translateFxml("""
            <?import javafx.scene.control.*?>
            <?import javafx.scene.layout.*?>
            <Pane xmlns:fx="http://javafx.com/fxml">
               <Label fx:id="label1" text="Hi!"/>
            </Pane>
            """, FxmlTranslator.class.getClassLoader());
        System.out.println(JavaCode.statements2String(javaCode));

outputs

javafx.scene.layout.Pane pane = new javafx.scene.layout.Pane();
javafx.scene.control.Label label = new javafx.scene.control.Label();
label.setId("label1");
label.setText("Hi!");
pane.getChildren().add(label);

Several important things are missing:

  • support for controllers
  • support for datatypes that don't follow bean conventions, but are implemented using JavaFXBuilderFactory
  • generating the code in a class during build

The first, shouldn't be difficult, the second may be tricky as I want to reuse JavaFXBuilderFactory's logic. The last is most tricky, as I don't know how Quarkus does such things.

@CodeSimcoe
Copy link
Contributor

Do you have a repo with your code ?
Maybe a derived extension (depending on this one) would be a start, I can get curious enough to try to give you a hand

@hallvard
Copy link
Author

hallvard commented Feb 7, 2024

@hallvard
Copy link
Author

I've now pushed some more code to the translator repository:

  • the FXML translator now supports more FXML features, in particular event handlers and controller initialization (fx:include is not supported, yet)
  • an annotation processor generates a "helper" class for each controller, that is used by the loader generated by the translator
  • a maven plugin that can run the translator during the generate-sources phase
  • a sample module that has the necessary setup for annotation processing and source generation

Together, this allows using FXML without runtime reflection. The snag is that code must instantiate the generated loader class, rather than the traditional FXMLLoader.

The question is still: what does it take to integrate this into quarkus, for the best developer experience.

@CodeSimcoe
Copy link
Contributor

I've had a look (before these updates)
I was trying to look at how we could generate the code at build time with a Quarkus extension.
My knowledge is rather limited about that, but willing to learn more. For now I lacked a bit of time to dive deeper into this.

Not sure that using the maven plugin is the ultimate goal (the extension could handle all of that, I guess).

I'll have a look when I get the opportunity to.

Anybody with Quarkus extension knowledge is welcome ;)

@hallvard
Copy link
Author

I tried to find examples of generating source code in a quarkus extension, but my impression is that they go directly to byte code (either explicitly, or using recording). The current translator has a java code model (AST like) that could just as well be used for generating byte code. Anyway, since I have no experience with quarkus extension, I found it was best to try to come as far as possible without quarkus-specific techniques, hence the source generator, annotation processor and maven plugin. A quarkus extension could continue from there or use the translator/processor more directly.

@MarceloRuiz
Copy link

MarceloRuiz commented Mar 15, 2024

I think this issue is a really good idea.
There is a project that might be useful for converting the FXML files into Java Classes:
https://github.com/garawaa/fxml2javaconverter
If the output class could be generated implementing an interface and with a user-defined annotation it could be picked up for Dependency Injection. If not possible, the user could simply extend the class and do whatever is needed.
Maybe a Gradle task could be created to transform the FXMLs into classes adding the output dir to the source set?

@hallvard
Copy link
Author

The fxml-template-processor repo mentioned above has been extended with such a generator, maven plugin and sample project. Please take a look at the sample.

I would love suggestions and help in properly integrating it with quarkus.

@hantsy
Copy link

hantsy commented Apr 6, 2024

Is there a project to write Fxml in other formats? such as Kotlin DSL, YAML etc. Personally I would like write it in Kotlin(UI layout and controller in one file) and discard the the fxml file.

@CodeSimcoe
Copy link
Contributor

CodeSimcoe commented Apr 8, 2024

I was thinking that using a @Recorder could do the job.
At build time, once FXML is parsed, we could use a recorder in which we would actually create FX elements and hierarchy.
Generated bytecode would then be executed at runtime.

Any FXML modification would require a new build (not just launching the app) but start time would be fast and without reflection, I guess.
Problem I have now with this approach is that we need a graphic environment (and an initialized Toolkit when creating elements).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants