You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As of now, there are the following "public api" artifacts:
api-public - just the annotations (API), no entry-point (EP - Yatagan object).
api-compiled - depends on api-public, adds Yatagan which loads compiled implementations.
api-dynamic - depends on api-public, adds Yatagan which loads dynamic implementations (can load compiled if configured) and comes with extra API for configuring reflection-specific things.
Now this "layout" seems to work, but apparently has some drawbacks. There's no real problem for the application developers, as they can just use the correct backend-specific artifact and be done with it. But when it comes to library development, some unpleasant things begin to surface: turns out it's not okay for library authors to depend on any backend-specific artifact, as they would actually force the backend usage on all the clients this way. One can't have multiple backends on the runtime classpath, as they would conflict. And the only backend-agnostic artifact is api-public which doesn't contain any Yatagan EPs, so there's no way for a library to create its own components in its code. If the library is not published and serves only as a way to organize code in the multi-module project, developers can get away with twiddling with tools like compileOnly/runtimeOnly and so on, but it is in no way a general solution.
So here I propose to rearrange the api/EP/loader code so its more practical to use and general integration logic can be formulated, maybe even with Gradle plugin.
New structure (draft)
api:public - contains both API and EP*. EP has the following behavior - they try to locate a dynamic loader class (once, the result is cached - very fast). If the loader is present, EP delegates to it. If not - EP loads the compiled implementations.
rt:loader - the dynamic loader class and the reflection engine - loads and serves reflection-based implementations.
*It's not yet clear how to handle reflection-specific API - should they be included into api:public and be no-op by default and delegate to the rt:loader?
This way every project module can depend on api:public and its code will compile regardless. And then, depending on the usage and settings, additional configuration can be done - adding code generators or rt:loader as runtimeOnly. This way published libraries can depend on api:public and ship with generated implementations of their internal components. While clients can still include rt:loader and use reflection backend for themselves.
If the project depends on a library, that uses yatagan internally and the project itself chooses to use rt:loader, then we probably don't want for this to affect the library behavior. In other words, how exactly should api:public's loader logic operate in regards to compiled implementations? Should it load them unconditionally if detected and try to use dynamic loader only if no compiled implementation is present? This works well in theory and preserves library behavior (if it comes with generated impls). But practice shows that build systems can leave stale generated code in the runtime classpath after a developer switched to reflection, e.g. via a build flag, then the loader would still use compiled impl instead of reflection. Need to give this some more thought.
The text was updated successfully, but these errors were encountered:
api-compiled is an empty artifact that just depends on api-public and is, essentially, deprecated.
api-dynamic depends on rt:engine and provides loader class implementation.
The crucial thing here is that api-compiled would be compatible with api-dynamic in a way, that the latter would take precedence if no compiled implementations are available.
As of now, there are the following "public api" artifacts:
api-public
- just the annotations (API), no entry-point (EP -Yatagan
object).api-compiled
- depends onapi-public
, addsYatagan
which loads compiled implementations.api-dynamic
- depends onapi-public
, addsYatagan
which loads dynamic implementations (can load compiled if configured) and comes with extra API for configuring reflection-specific things.Now this "layout" seems to work, but apparently has some drawbacks. There's no real problem for the application developers, as they can just use the correct backend-specific artifact and be done with it. But when it comes to library development, some unpleasant things begin to surface: turns out it's not okay for library authors to depend on any backend-specific artifact, as they would actually force the backend usage on all the clients this way. One can't have multiple backends on the runtime classpath, as they would conflict. And the only backend-agnostic artifact is
api-public
which doesn't contain anyYatagan
EPs, so there's no way for a library to create its own components in its code. If the library is not published and serves only as a way to organize code in the multi-module project, developers can get away with twiddling with tools likecompileOnly
/runtimeOnly
and so on, but it is in no way a general solution.So here I propose to rearrange the api/EP/loader code so its more practical to use and general integration logic can be formulated, maybe even with Gradle plugin.
New structure (draft)
api:public
- contains both API and EP*. EP has the following behavior - they try to locate a dynamic loader class (once, the result is cached - very fast). If the loader is present, EP delegates to it. If not - EP loads the compiled implementations.rt:loader
- the dynamic loader class and the reflection engine - loads and serves reflection-based implementations.*It's not yet clear how to handle reflection-specific API - should they be included into
api:public
and be no-op by default and delegate to thert:loader
?This way every project module can depend on
api:public
and its code will compile regardless. And then, depending on the usage and settings, additional configuration can be done - adding code generators orrt:loader
asruntimeOnly
. This way published libraries can depend onapi:public
and ship with generated implementations of their internal components. While clients can still includert:loader
and use reflection backend for themselves.If the project depends on a library, that uses yatagan internally and the project itself chooses to use
rt:loader
, then we probably don't want for this to affect the library behavior. In other words, how exactly shouldapi:public
's loader logic operate in regards to compiled implementations? Should it load them unconditionally if detected and try to use dynamic loader only if no compiled implementation is present? This works well in theory and preserves library behavior (if it comes with generated impls). But practice shows that build systems can leave stale generated code in the runtime classpath after a developer switched to reflection, e.g. via a build flag, then the loader would still use compiled impl instead of reflection. Need to give this some more thought.The text was updated successfully, but these errors were encountered: