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

Request for feedback: Discontinuing support for dart:mirrors #44489

Open
mit-mit opened this issue Dec 16, 2020 · 99 comments
Open

Request for feedback: Discontinuing support for dart:mirrors #44489

mit-mit opened this issue Dec 16, 2020 · 99 comments
Labels
area-meta Cross-cutting, high-level issues (for tracking many other implementation issues, ...).

Comments

@mit-mit
Copy link
Member

mit-mit commented Dec 16, 2020

The Dart mirrors core library (dart:mirrors) is currently in a sub-optimal state:

  • It is marked "experimental" in the list of core libraries

  • It is marked "unstable" in the API docs

  • It is unsupported in the Dart web platform

  • Is is disabled from use in the Flutter framework (and has been so for years)

We've investigated potentially making mirrors a stable, fully supported offering, however in its current form this has a few large challenges:

  1. Dart has powerful "tree shaking" for ensuring we can produce apps with small code sizes. This relies on the ability to detect unused code, which doesn't work if reflection can use any of it dynamically (it essentially makes all code implicitly used).

  2. The APIs supported by mirrors are significantly behind the current Dart language, for example they do not support extension methods or null safety.

We believe that one of the core uses of mirrors is to do various forms of code generation and meta-programming. Before we potentially fully discontinue mirrors, we'd like to better understand the desired use cases here so that we could have alternative solutions and migration path ready for current users before we make any changes to dart:mirrors. If you have any such use cases, please leave a comment.

@mit-mit mit-mit added the area-meta Cross-cutting, high-level issues (for tracking many other implementation issues, ...). label Dec 16, 2020
@daniel-v
Copy link

To make sure the info gets to you, here is my response to dart-misc mailing list:

I might be reading too much into this but discontinuing dart:mirrors library has some serious consequences that were not mentioned in your mail. The most important is that we'd lose the ability to do runtime reflection at our server-side code. It is an HTTP server that uses mirrors for the most important bits of it. Transitioning away from it is a scary proposal for many reasons.

If anything I'd request to add mirrors to AOT snapshots too, instead of discontinuing, so that we'd get to deploy our code as a single binary with all the benefits and drawbacks it means. But... Let me address the concerns I have:

  • There is no proposal as to what would replace it, we need more info what you had in mind.
  • If there is no replacement, you'd effectively eliminate some useful features from the language/runtime we actively use. I don't think that's the case; I will assume for the rest of the mail that you are thinking in terms of migration to code generation.
  • We enjoy the ability of quick iterations on BE code without having to run builders all the time, which adds a lot of compile time overhead.
  • dart:mirrors is easy to learn and test, package:build is most definitely not
  • I keep struggling with different build_runner versions on FE code where package:analyzer constraints prevent me from using certain packages together (eg. Angular 5.3 and freezed). This issue does not exist with runtime reflection.
  • Builders can be really slow, in some cases I'd prefer the runtime overhead instead of the added minutes of wait time for the builders to complete.

I'm sure you have a list of pros and cons for mirrors vs. code generation too. Having said that, I'm not against the idea completely if there are good enough reasons.
I'd have to bite my lips hard if mirrors were removed today and would have to migrate to pacakge:build but luckily we still have some time.

There have been several precedents where such a change was preceded by preparing users, crafting new tools or giving some extra attention to alternatives. Most recent one is the NNBD tooling to this. I hope it is the case here as well, in which case feedback on why I'm nervous about moving to pacakge:build is warranted here. In my opinion, build and associated packages are in a bad spot right now.

My recent incursions to writing builders were ... trying because:

  1. after awhile, I gave up trying to find written documentation for the questions I had, I started digging around in the code - this shows that the there is too little information out there about how to write builders, how it works and how to configure them. Eg. I was surprised to see that build.yaml and build.config.yaml are merged together instead of being taken as an absolute configuration BUT with a huge caveat: builder declarations are ignored from build.config.yaml.
  2. the configuration space is very complex and the tools do not aid me rooting out configurations that make no sense. ( ref ) Figuring out what is OK and what is not can only be done through trial and error or by reading the implementation. This is a problem for every user of package:build, not just builder authors.
  3. Contributors of package:build stated it is intended for "superusers" ( ref ). Context to it is: I brought up that it is difficult to map how I see code as a developer to how analyzer sees the same code, then write a builder around that mapping. dart:mirrors make it fairly easy. I fear that I would be required to run around in language specifications to figure out what is what for a question which as simple as: What is the first method in this class with an annotation of Route('/')? Example of how complicated it can be just to resolve types here (used by Angular Compiler too).

I've had some steep learning curves over the years. package:build was one of them. The primary reason - which I consider the most important of all - is a conceptual one: Code generation is becoming more and more important piece of the language ecosystem. As such, as a user of the language, my expectation is that such an important piece is well-documented, somewhat fool proof and fairly easily approachable.

I hope that this is the kind of feedback you were looking for to help you make the right decisions.

@mit-mit
Copy link
Member Author

mit-mit commented Dec 16, 2020

To those downvoting, it would be great if you could also post a comment with what concerns you. As mentioned, we'd like to understand the use cases where you use mirrors today, so that we can investigate if there is an alternate way by which we can support those use cases without mirrors.

@zmeggyesi
Copy link

I work with @daniel-v and @gothamgasworks - I can second Daniel's input that removing runtime reflection would impact us quite negatively.

Our product actively uses runtime reflection in server-side Dart code, both in production and in development, and our preference would be to retain the ability to introspect code rather than rely on build-time code generation, both in the interest of development cycle times and ease of understanding of deployed code.

@gothamgasworks
Copy link

@daniel-v 's summary is pretty good, I agree with them.

Some things I want to emphasize:

  1. Quick iteration, because we don't need to wait for builds (we use kernel snapshots on the production server)
  2. No need to develop builder code for every use case that may arise (we encountered quite a couple)
  3. Builder code development is a drag, we have to constantly update the code compared to our mirrors code, which we only had to touch once (moving from Dart 1 to 2)

@gothamgasworks
Copy link

We use dart:mirrors in:

  • Database record class representation
  • Data merging tasks (like statistics, or updates coming from external systems)
  • Mapping server side business object to client side business objects
  • HTTP request routing
  • Serialization libraries (object serialization, object factory registration, XML serialization required by some external systems)
  • Report generation system

Of course we could work around it with builders, I would just like to be able to not work with the builder/analyzer API, which requires a serious amount of maintenance as opposed to the mirrors API.

@isoos
Copy link

isoos commented Dec 16, 2020

The use cases I had in the past month:

  • package:appengine uses reflection for entities that are stored in Datastore.
  • package:json_rpc_adapter uses reflection for both HTTP-client and HTTP-server JSON-RPC, by taking the abstract class Api { Future<Rs> operation(Rq rq); ...} like API definition and binding it to both.

My wish-list, which would probably make the lack of dart:mirrors bearable:

  • Fast and simple-to-use source code parser library and good AST/API to extract the annotations, classes and methods that are of the interest of the tool (without specifying upfront which parts we are interested in). My problem with package:source_gen and related build-utilities is their complexity: they are two levels up the chain, they use the framework approach (having a single but somewhat complex way to do things), as opposed to a library that exposes the data and allows the tool to process it as they want. package:analyzer is not really without ceremonies either, but at least it is "only" a library. Sometimes I don't even need to explore the types recursively, I just need the field/methods's name the parameter names and their types. It is not an issue if the full source graph is not available right away, in many cases it may be a (performance) benefit.

  • A separate source code generator, that is not tied to any convention (e.g. not every project wants to use part for the generated files, nor do they necessarily want to place only a single file in the same directory as the source). I have done at least 4 separate code generators in the past years, and they had different requirements about these, and it was just easier to do String concatenation + custom file placement + calling dartfmt than to use any of the big frameworks. I want to build my source code in a programmatic way, the library can enforce the structure (class, method, function boundaries) and format it nicely, and that's all it needs to do. E.g. source_gen examples are full of generators, which assume that I want only to plug in a small piece somewhere. Instead, it should just take the tree as I build it.

  • A trigger mechanism which detects when the generator chain needs to run. I think the situation here is rather good, and I don't have much preference: package:build_runner's approach may be okay (if made simple for generic tool use), or even package:watcher can be used if the programmatic approach is preferred by the developer.

  • A more convenient way to live-reload classes in the Dart (JIT) VM. package:vm_service + package:watcher should be really a VM flag that can trigger a reload of classes locally, without further configuration or without adding dependencies to the application under development. (Yes, it may not cover 0.1% of the changes that may happen, e.g. depending on a new package, but restarting the local dev app every once in a while is not an issue. Restarting it every time is.)

Summary: To replace dart:mirrors, the following tools need to be available:

  • parse and expose parts of the code efficiently
  • generate source code in a convenient way
  • (trigger source code generator when the inputs have changed)
  • reload the source code in the local dev VM without long ceremonies

@scheglov
Copy link
Contributor

The Dart Analyzer team uses package:test_reflective_loader extensively for all our tests, and it is based on using mirrors. We found out early that vanilla package:test is not structured enough for complicated package:analyzer tests.

@lexaknyazev
Copy link
Contributor

My use case of reflection is docs generation via iterating over class members and invoking them to get sample outputs. The code using dart:mirrors resides in a grinder task thus being completely unaffected by the lack of support by web and AOT runtimes.

@jakemac53
Copy link
Contributor

I have mentioned this elsewhere but worth mentioning here for posterity - build_runner uses dart:mirrors in order to discover all of the libraries that exist in a program. In our case that is the build script itself, and we use that to know if we need to invalidate the snapshot.

@gmpassos
Copy link
Contributor

I think that a new framework for reflection should exists. For now we should kill dart:mirrors.

In the future, after NNBD, a new framework should exists with another name. One where you can mark classes that should have their full description after compilation (the main information needed for runtime reflection). The marked classes could be by annotation or passing a list of classes for the "refletor factory" that should be tracked, allowing any class to be tracked for reflection. With that approach,Dart tree shaking won't be affected, unless you really need a reflected class for your project.

@nex3
Copy link
Member

nex3 commented Dec 16, 2020

dart:mirrors is used by Grinder, which is the best (and only) task runner for Dart with an 84% popularity rating on pub.dev, and my the mustache package which is used by both Dartdoc and the 99%-popularity google_fonts package. Both of these are load-bearing ecosystem packages. Removing dart:mirrors without a clear plan for something similarly usable for these packages would cause a lot of downstream pain and chaos.

It's worth noting that @isoos's proposed solution would likely not be sufficient for Grinder, since it's infeasible to have a code generation step as part of a task runner workflow.

@tvolkert
Copy link
Contributor

This (a) doesn't solve all the problems with dart:mirrors, and (b) would only cater to specific use cases, but leaving the idea here nonetheless... what if we were to still tree-shake as normal even in the presence of dart:mirrors, and rely on developers marking methods as @pragma("vm:entry-point")?

@DavidLiedle
Copy link

My use case is... well, hold on just a dang minute.

I don't think I should have to lay out my thinking process to express the fact that I value the presence of dart:mirrors in the language. This "what is your use case?" approach smells of management and budgeting issues; not of nurturing the Dart ecosystem with an optimistic eye on the 10-20 year future of the language.

The APIs supported by mirrors are significantly behind the current Dart language, for example they do not support extension methods or null safety.

So allocate the dev resources to get on that. "Significantly behind" is a prioritization issue.

This is plainly technical debt. Defenestrating a sub-section of your server-side users building ORMs (oops, there's my use case..) would be an easy way out, and clearly a cheaper path forward. But seriously, cheaper over better? Not very Googley, imho. Do the right thing. Strive for excellence (and make the one thing you're doing really well be creating a language that can be used everywhere, including places where reflection makes sense). Keep an eye on the goals (both short and long term). Be proactive (and bring dart:mirrors up to par). Go the extra mile... Well, you know all this. You're the ones that work there.

I've been one of the earliest, most enthusiastic, and staunchest allies of both Dart and Flutter, but the bloom is off the rose with several prioritization choices that make me just, well, nervous. I'm tired of building things in AngularDart, or Polymer.dart (oy..), and then getting downstream just enough to feel the aging, unfinished edges and have to admit to myself that it's not a viable path forward. I have had to abandon chunks of my work because of hitting these dead ends and faded photos of what was planned. It's turning my attention to other places, and mine has teh sad. It took this thread for me to even glance back at AngularDart and see that it received an update in October of this year. Yay! But.. I can't trust it; already tried that, and got left behind.

TL;DR: this decision will impact the trust of your user base regarding your team's intentions in general; trust that is already flying on one engine. If we smell even a whiff of "Flutter doesn't need it so it's getting de-funded", even the features you do nurture will be untrusted for any other use case, and weaken your entire value proposition. Many of us, even your biggest fans, will simply walk. Not a rage-quit, but a pragmatic step out of what would appear to be a populism-driven-dev world and into other environments that focus on remaining well-rounded. Over time. For everyone.

The End

@zmeggyesi
Copy link

@gmpassos

For now we should kill dart:mirrors.

Not without a replacement being widely available and adopted we shouldn't!

Evidently, mirrors and reflection as a whole is widely used, in load-bearing packages as @nex3 points out, not to mention in the major task runner of the ecosystem. Killing mirrors without a replacement at this point pretty much breaks the ecosystem; killing it with a replacement that's not (near-)universally adopted does the same.

If the team moves forward with deprecating and removing this, it should only be done after they provided a replacement framework/solution, and the majority of the ecosystem, or at least its core components, have transitioned to the new way of doing things.

@zmeggyesi
Copy link

The more I think about code generation versus reflection/introspection, the more convinced I am that codegen as a whole, regardless of language, is a very double-edged sword (and that's leaving aside the fact that most generated code is fugly, but the saying is usually that you don't need to read it anyway - whether that's true is another thing entirely).

Either you persist the generated code, in which case you get the benefit of short build times, but you trade storage for it (you're persisting essentially redundant code) and you risk the generated code drifting from the handwritten code it's supposed to work with; or you ignore it (as in .gitignore it) and always regenerate it at build-time, in which case you lose fast builds, unless the build system can do some very clever incremental build magic, and you now have a piece of your application that is ephemeral and you lose the ability to fully understand the application based on the code alone, in exchange for the generated code always cooperating with the handwritten surroundings correctly.

Either way I slice it, I don't really see upsides to code generation as opposed to runtime reflection/introspection.

@mraleph
Copy link
Member

mraleph commented Dec 17, 2020

I think original post by @mit-mit was a bit unclear: we have no plans to deprecate and remove dart:mirrors before we fully understand its current uses and at the very least have an alternative solution and a migration path (if necessary) for the current users. I took a liberty to edit @mit-mit's post to highlight this.

For me an ideal outcome would be that we end up staffing an effort to build a solution that covers dart:mirrors use cases, has similar usability/performance characteristics (no need to fiddle with code generation), simple migration path and works in AOT scenarios.

Another possible outcome is that we just invest some effort in bringing dart:mirrors in alignment with Dart 2.12 features.

Figuring out where we go requires feedback from current users of the library, so thanks to everybody who has provided it.

@zmeggyesi
Copy link

we just invest some effort in bringing dart:mirrors in alignment with Dart 2.12 features

I would very much like that, yes.

@mit-mit
Copy link
Member Author

mit-mit commented Dec 17, 2020

Thanks for all the feedback so far! Let me elaborate a bit, and answer a few questions:

@mraleph wrote:

I think original post by @mit-mit was a bit unclear: we have no plans to deprecate and remove dart:mirrors before we fully understand its current uses and at the very least have an alternative solution and a migration path (if necessary) for the current users.

Agree, the goal of this feedback issue is for us to better understand a) what critical uses mirrors have today, and b) based on those use cases, if we could design an alternate mechanism that could satisfy those same uses, but potentially be better aligned with our compiler goals.

@daniel-v wrote:

There is no proposal as to what would replace it, we need more info what you had in mind.

Correct, we don't have such a proposal yet. As I wrote above, we're hoping to learn how mirrors are being used today, and from that we'll potentially design an alternate mechanism, which we could then share for feedback.

@cedvdb
Copy link

cedvdb commented Dec 17, 2020

  1. Dart has powerful "tree shaking" for ensuring we can produce apps with small code sizes. This relies on the ability to detect unused code, which doesn't work if reflection can use any of it dynamically (it essentially makes all code implicitly used).

@mit-mit Why ? Are you implying that if reflection is used, code that has nothing to do with what it was used with can't be tree shaken ? If I use reflectClass(SomeClass) , my whole codebase can still be tree shaked, minus that specific class. Alternatively, you could still tree shake that class and the reflection happens on the tree shaked class (assuming properties are tree shaked).

@jakemac53
Copy link
Contributor

jakemac53 commented Dec 17, 2020

If I use reflectClass(SomeClass) , my whole codebase can still be tree shaked, minus that specific class.

Not with the current dart:mirrors. From a single ClassMirror you can reach not only that class, but all classes referenced by that class in its return types and parameter types, etc.

And actually, its worse than that because you can also use the owner getter to get a mirror of the entire library, which exposes effectively all the transitive imports of the library.

This is just a part of the problem with the current dart:mirrors implementation, the ability to get from one mirror instance to just about anywhere else in the program through imperative code that is difficult to track the usage of (especially in the presence of anything dynamic).

@srawlins
Copy link
Member

To expand on what @scheglov mentioned about test_reflective_loader: it uses a small piece of mirrors, but there is not another (codegen-free) solution:

The system is fed test classes with defineReflectiveTests(MyTest), and class MyTest has a series of test cases, which are each a public method on the class. So we use reflectClass, ClassMirror.metadata, and then ClassMirror.instanceMembers to get all of the test cases. We pass each MethodMirror to package:test by fetching the method as a field, and then invoking it with InstanceMirror.invoke. The ability to annotate test methods with @solo or @failingTest is important for our use case, so being able to grab a class's members and examine annotations are the core parts of test_reflective_loader's uses.

@rdnobrega
Copy link

I use mirrors for ORM, JSON serialization, documentation.. Reflection is a really big thing and should be a core part of Dart. Also it should be able to use in dart AOT.

Build_runner is not an option. If mirror is "experimental" build_runner is still a "thought" on production level. Too complicated to use and too low level to have actual value for programmers not interested in how the analyzer works. I mean, "I just want to get the properties' names and types, not learn how the dart analyzer works!". Also, it lacks documentation and greatly increases compile time.

I don't know how complicated the relation tree shaking-reflection is, and woudl love to read more about it, but since reflection could potentially access parts of code that was shaken off, it could have a property entitled "isShaken", so that devs can expect a specific element to exist but was shaken off, and trying to access it would raise an exception. To avoid exceptions, another method for reflection could be implemented like "reflectSafe()" which doesn't reference any part of dead code at all.

I don't like the idea to annotate members to be able to reflect, because it seems a downgrade for the current lib, but it's better than abandon reflection altogether.

@Jonas-Sander
Copy link

Just as a thought: With the advances in the Dart ecosystem on the Flutter side I think it is save to say that Dart will also have more adoption on the server side.
A recent example is the Functions Framework for Dart which you can use to run code in Google Cloud Run, Google App Engine and Knative-based environments.
So there may be even more desire for something like dart:mirrors in the future.

@rdnobrega
Copy link

@mit-mit Is there any news about this topic?

@mit-mit
Copy link
Member Author

mit-mit commented Jan 25, 2021

Thanks for all the feedback so far, it's very helpful!

We're currently doing some very initial brainstorming and exploration of some potential features in the meta-programming space. We speculate that they might support the kinds of use cases we're seeing in this thread, but it's too soon to tell with any certainty yet. We're going to explore that a bit more, and then share. For now, we're not making any decisions about mirrors until we understand the space a bit better.

@RaedDev
Copy link

RaedDev commented Mar 9, 2021

I want to add my voice to the rest of the voices, my use case is to load modules dynamically and use the functions they provide.
While this use case and most other use cases could be covered by code generation it's a trade of between more complex build process, and a tree-shaken code.

As many people have said, marking classes that should support reflection is great way to solve many problems, and having a flag to enable reflection on the whole project when the value of mirrors outweigh the costs.
Rather than forcing one way of doing things that might not suit every use case.

@gmpassos
Copy link
Contributor

gmpassos commented Mar 9, 2021

I think that the biggest problem with reflection is the collateral effect with the tree shaking.

To minimize the problem, I think that the Reflector (the class that will use reflection, that will use use a Class X by reflection), should be the one that defines the classes that will be reflected (accessed by reflection), and not the opposite.

I saw in the past some frameworks where the class that will be reflected is self marked (usually with an annotation in the class). Actually reflection should exists to access any class, in the project or in other packages! This is the main mistake of the reflection frameworks in Dart, the miss conception that a reflected class should be marked in the class sources code itself.

If the reflector is the one that defines, statically, the classes to reflect, the side effect in the tree shaking will exists only in the marked classes for reflection.

A well defined interaction of reflection and tree shaking is the most important base for the whole framework, but no one is discussing this.

@jakemac53
Copy link
Contributor

@gmpassos I very much agree with you :). I have even hand coded something to mimic a theoretical "const reflection" proposal in this package https://github.com/jakemac53/static_reflection (sorry for lack of readme etc there). You can see that it does indeed not hinder tree shaking in the same ways that dart:mirrors or reflectable do. It is also a bit more cumbersome to use but I think the tradeoffs are worth it.

However this issue is more about just gathering use cases than it is brainstorming solutions :). Primarily we want to understand if a feature such as const reflection objects (note this is far from a real proposal) would solve all of peoples use cases.

@jakemac53
Copy link
Contributor

jakemac53 commented May 11, 2023

yes macros are ok but I think not ready yet :)

Yes, this issue exists largely to track the use cases for mirrors today, to drive the necessary features that we would need from macros/codegen/etc in order to one day drop support for mirrors (or determine it wouldn't be possible to do so).

@bradcypert
Copy link

bradcypert commented Jul 2, 2023

@jakemac53

I maintain a server-side framework, and the main reason I chose reflection over code gen was to simply not force the user to think about generating code. Configuring build runner is a pain, and for newer devs it can be very confusing.

All I would need to drop reflection support would be static meta programming. I understand that there's definitely other use cases out there where reflection would be harder to leave behind though.

Edit: I've also considered maintaining my own CLI that would take care of the codegen for you -- I'm still not sure how I feel about this, but it's something I've been exploring.

@Nexushunter
Copy link

I would love to be able to retain the dart:mirrors library. I'm part of a group that is working on providing an OpenAPI generator annotation that allows for a config to be built in dart and then generate the library based on it. Currently we provide a way to pass headers into the generator for privately hosted specs. But it isn't amazing (read: very naive). We are providing a generic delegate class that can be extended and then passed in. Due to the nature of being able to provide both a positional argument and named args in a subclass constructor it becomes difficult to provide support for reviving that object. I've opened this PR within source_gen to begin allowing for that but that implementation relies on dart:mirrors to properly rebuild the object. While it is a WIP it makes sense for builders / generators to be able to use a fully qualified version of their annotation as needed.

@jakemac53
Copy link
Contributor

@Nexushunter it seems to me like you could probably code generate everything you need to be able to revive those constants?

@Nexushunter
Copy link

@Nexushunter it seems to me like you could probably code generate everything you need to be able to revive those constants?

@jakemac53 I'm not looking to output it to a file 🤔 I'm looking to do an in memory revive. I think I might not be understanding what you mean. Can you expand a bit?

@jakemac53
Copy link
Contributor

@Nexushunter it seems to me like you could probably code generate everything you need to be able to revive those constants?

@jakemac53 I'm not looking to output it to a file 🤔 I'm looking to do an in memory revive. I think I might not be understanding what you mean. Can you expand a bit?

For each constant you are trying to revive (each class used as an annotation), you could run a separate code generator (or eventually, macro), which would generated the code for reviving that particular class. Similar to generating a toJson/fromJson for a class.

@Nexushunter
Copy link

@jakemac53

Ahhh I see what you're saying. In this case I don't think that would be viable.

It seems that way to me because:

  • consumers are potentially going to be extending our implementation and the builder shouldn't be forced to rely on externally generated code
  • source code generation shouldn't be needed to restore the object to it's original form when all of the required data is present in memory already
  • it feels hacky to generate source code for something we already have the source for and just want an in memory representation based on the serialized value provided.
  • the solution to this problem only needs to be implemented once and should be available within the core library since the processing is being provided in the core library.

While there are lots of amazing use cases for generating source code this doesn't feel like one. We have the source and can rebuild the object in memory based on the DartObject and dart:mirrors fully enables this.

I would rather see an updated and stabilized API over the removal because there are valid use cases.

@jakemac53
Copy link
Contributor

  • consumers are potentially going to be extending our implementation and the builder shouldn't be forced to rely on externally generated code

If you consider a sufficiently well integrated code generation mechanism (part of the compiler) I think most of these concerns go away.

  • source code generation shouldn't be needed to restore the object to it's original form when all of the required data is present in memory already

But if we were able to remove dart:mirrors, that required data would no longer necessarily be present.

  • it feels hacky to generate source code for something we already have the source for and just want an in memory representation based on the serialized value provided.

The "source code" in this case is actually a very abstract representation, not really any different from JSON or some other serialization format. The types you see are not even necessarily available in the current isolate and thus impossible to rehydrate at all.

So, I would actually say it is less hacky to rely on the explicitly generated code to rehydrate very specific known types.

  • the solution to this problem only needs to be implemented once and should be available within the core library since the processing is being provided in the core library.

I agree this is the advantage of mirrors in general - you can write something very general purpose and it has a decent chance of succeeding. When it fails though its going to be a lot harder to debug, and then all the other downsides of mirrors regarding tree shaking and performance (although it can be possible to get decent performance if you are very tricky, most of the time its pretty bad in practice).

I would rather see an updated and stabilized API over the removal because there are valid use cases.

Mirrors are one way to solve metaprogramming, but not the only way. So most of this issue is about understanding the use cases, and what the tradeoffs would look like if we went with a different approach.

I do agree moving your example to some form of code generation, however well integrated, would be a somewhat degraded development experience. It would be possible, and more statically safe, but would involve some extra boilerplate as well.

@Nexushunter
Copy link

@jakemac53
Thanks for the patience.

The "source code" in this case is actually a very abstract representation, not really any different from JSON or some other serialization format. The types you see are not even necessarily available in the current isolate and thus impossible to rehydrate at all.

I would be inclined to disagree. While yes the types aren't necessarily available in the current isolate, the library and it's declarations are. With knowing the library and declaration name (which we currently get from dart:mirrors) you can rehyrdate it with relative ease.

So, I would actually say it is less hacky to rely on the explicitly generated code to rehydrate very specific known types.
How would the generating/builder library know about this code?

If you consider a sufficiently well integrated code generation mechanism (part of the compiler) I think most of these concerns go away.
I may not have been sufficiently clear here looking at it now. To expand a bit more say we have:

/// Default [RemoteSpecHeaderDelegate] used when retrieving a remote OAS spec.
class RemoteSpecHeaderDelegate {
  const RemoteSpecHeaderDelegate();

  Map<String, String>? header() => null;
}

Defined and exposed in our library. The consumer would define a subclass like:

// within my_spec_delegate.dart
import 'package:spec_builder/spec_builder.dart';

class MyRemoteSpecHeaderDelegate extends RemoteSpecHeaderDelegate {
    const MyRemoteSpecHeaderDelegate(): super();

    @override
    Map<String, String>? header() => {'Authorization':'someAuthToken'};
}

// within spec_dfn.dart
import 'package:spec_builder/spec_builder.dart';

import 'path/to/my_spec_delegate.dart';

@SpecBuilder( // Imported from builder library
    delegate: MyRemoteSpecHeaderDelegate(),
    // some other properties
)
class SpecDef {}

In this case the consuming repo is building off of the base we delegate we provide, but they aren't implementing the builder themselves, and are having us use it in a predictable manner. I'm not sure how this would be achieved outside of dart:mirrors (obviously we don't current have another option

But if we were able to remove dart:mirrors, that required data would no longer necessarily be present.

True, but I'm providing a use case for leaving the library which this comment is ignoring.

Mirrors are one way to solve metaprogramming, but not the only way. So most of this issue is about understanding the use cases, and what the tradeoffs would look like if we went with a different approach.

Correct and that is what I'm trying to point out. This is a valid use case and is only something that really needs to be done on the developer side (builders are dev_dependencies, unless you're building; at least I would expect that to be the common case, I could be wrong here).

I would love to hear another potential solution on how that may be applied to a builder or how that might be achieved without a build (obviously nothing concrete).

(I've started to read dart-lang/language#1482 but I'm gonna need to make a couple passes before I can sus everything out and understand it more indepth)

@jakemac53
Copy link
Contributor

jakemac53 commented Sep 25, 2023

With knowing the library and declaration name (which we currently get from dart:mirrors) you can rehyrdate it with relative ease.

Only if the build script actually includes the library and declaration though. I don't understand how those are being included in your case, without the user going through all the other builder related config to set up their own builder?

My guess is that in your case, you are actually all within one package, and your builder_import is importing a library which also imports the my_spec_delegate.dart library? I don't understand how it would be working otherwise.

This is all mostly orthogonal though - I think the use case you are describing is well understood - it is effectively identical to the serialization/deserialization use case. With mirrors you can create a general purpose mechanism for "rehydrating" (or "dehydrating" :P) objects.

If we do add expression level macros, you could get something similar. But it would be based on the static type of an object and not its runtime type, which can be a pretty big distinction.

@ykmnkmi
Copy link
Contributor

ykmnkmi commented Sep 25, 2023

I use mirrors in utilities like walk(astNode, enter: enter, leave: leave) and serializing classes in tests.

@bobjackman
Copy link

For us, mirrors are crucial for many tests so we can inspect/manipulate values of private/protected properties.

@insinfo
Copy link

insinfo commented Oct 6, 2023

I use dart:mirrors in several backend and CLI projects. I think that even if expression level macros were implemented in Dart there are always use cases where reflection can be much more useful for the developer

@ykmnkmi
Copy link
Contributor

ykmnkmi commented Oct 6, 2023

I don't know if macros can do this? but I want to get all annotated top level members:

@Injectable(providedIn: 'root')
final class UserService {}

@RootInjector() // macro generated constant class
external Injector get rootInjector;

T inject<T>() {
  return rootInjector.get<T>();
}

final userService = inject<UserService>();

@jakemac53
Copy link
Contributor

jakemac53 commented Oct 6, 2023

I don't know if macros can do this? but I want to get all annotated top level members:

I don't know how the providedIn: 'root' part of that is supposed to work, but generally it is possible to create a dependency injection framework similar to this. I have one example usage here https://github.com/dart-lang/language/blob/main/working/macros/example/bin/injectable_main.dart (note you can't actually run this yet).

If your example above is searching a directory for files with certain annotations that portion of it would not be possible, but you could use a slightly less magic API such as the example above.

@ykmnkmi
Copy link
Contributor

ykmnkmi commented Oct 6, 2023

@jakemac53, rootInjector is a private member of the DI package, and UserService is my class. So I need to get my user service from the DI package using the inject function.

copybara-service bot pushed a commit that referenced this issue Oct 24, 2023
This test has flaked on our CI but actually it's consistently failing
because we do not represent `typedef`s in a reflective way in the VM
since switching to kernel (because they used to be desguared on kernel
level).

So this has not been working for a long time, let's remove this test.

Issue #44489

Change-Id: I36e8d8aee7a7736e8afc5741b01d10429ac0d7a9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/331941
Commit-Queue: Martin Kustermann <kustermann@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
@erickib
Copy link

erickib commented Jan 2, 2024

I started to learn mirrors so I want to check here is really been obsolete/deprecated? I was planning to create a small MVC framework. What can I use instead for example loop a user defined 'controller' files in a directory and instantiate object calling users defined controller class/methods from those files.

@mraleph
Copy link
Member

mraleph commented Jan 3, 2024

I started to learn mirrors so I want to check here is really been obsolete/deprecated?

Not yet, though we heavily discourage the use of mirrors as they are not really compatible with closed-world AOT compilation. They don't work on the Web or in Flutter so you can only use them in CLI in JIT mode.

We have not been updating the library with respect to new language features: e.g. you can reflect on a record but you can't get record's shape (because there is no corresponding RecordTypeMirror) which makes it somewhat useless.

So my recommendation would be: don't use mirrors for anything critical.

@natebosch
Copy link
Member

natebosch commented Jan 4, 2024

What can I use instead for example loop a user defined 'controller' files in a directory and instantiate object calling users defined controller class/methods from those files.

If you are describing dynamically loading dart code from a directory of dart libraries, this is not a capability offered by mirrors.

Edit - I missed the obvious API for this on IsolateMirror 😳

@mraleph
Copy link
Member

mraleph commented Jan 4, 2024

If you are describing dynamically loading dart code from a directory of dart libraries, this is not a capability offered by mirrors.

It is: IsolateMirror.loadUri is the API for that.

@bobjackman
Copy link

bobjackman commented Feb 17, 2024

Just my 2 cents: I agree that use of mirrors for main application should be discouraged, due to incompatibility with AOT compilation. HOWEVER, mirrors play a critical role in our automated testing suites allowing us to verify functionality of things otherwise inaccessible (since tests are outside the test-target lib, tests can't see private members/methods, and therefore can't exercise them (or verify they were called as expected)). Deprecating mirrors would be a massive regression for us.

@francescovallone
Copy link

Good afternoon 👋!

Sorry to bother, but I'm trying to understand a little bit more about on the situation of dart:mirrors.

I understood from this issue that right now dart:mirrors shouldn't be used in any critical case. I'm really fond of using reflection both for backend and object serialization (dartson-like) but I'm a little bit concerned about the current state of the library since as stated by @mraleph there is not a way to get the shape of a record for example.

Can you give a more detailed update on what plans dart has for this specific library for example if it will be discontinued in a more permanent way in the near future or if the team is trying to provide an alternative?

@jakemac53
Copy link
Contributor

I can't speak with authority, I do not control such things, but I believe it is unlikely it would be removed in a non-breaking Dart language release (so, Dart 4 at the earliest). But, it also is unlikely to get many improvements such as knowledge about record types. It has always been an experimental/unstable API and never graduated to full support. We strive mostly not to break the existing use cases, until such time as there is a viable replacement. This issue exists primarily as a way to track those use cases that exist, such that we can make decisions in the future about the support of this API, as well as the features needed to make any replacement a viable one.

@lrhn
Copy link
Member

lrhn commented Feb 26, 2024

I wouldn't say that dart:mirrors has always been experimental. In Dart 1, it was fully supported, also on the web.

That was at a significant cost, though, which was why one was strongly discouraged from actually using it on the web, and if you did, there were annotations that allowed some tree-shaking to happen anyway. It still broke tree-shaking in general and required extra metadata, so if just one corner of your web app uses it, your deployment size could grow a lot.

Rather than worry about that, it was discontinued on the web, and never supported in AoT, and because of that dart:mirrors has not had any further development since about Dart 2.0. That's when it became "effectively unsupported". New compilers didn't need to support it, new platforms (including Flutter) didn't.
It's a feature that only works on the JIT compiled VM, and only really with features that existed in Dart 2.0. That means you can use it for scripts (if you don't AoT compile them), and not much else.

Using reflection in tests means that those tests cannot be run AoT compiled, which means they are not testing the code that's actually going to run in production. (Can't do dart test -c exe.)

You can use dart:mirrors today, but you shouldn't.
They only work in stand-alone programs that you can control how gets run.
They should never leak into other people's code, or be run by any system that migth one day want to compile the code to exe first.

I do hope that macros can give users what they need to replace existing uses of reflcection. Like a reflectiveTest that finds all methods with a name starting with test, a macro could find the same methods, and create an allTests() method that calls them one by one.

@sigurdm
Copy link
Contributor

sigurdm commented Feb 26, 2024

In Dart 1, it was fully supported, also on the web.

https://codereview.chromium.org//61793006 marked it as "unstable" - that was 2013

The documentation (today) says: This library is only supported by the Dart VM and only available on some platforms.

@lrhn
Copy link
Member

lrhn commented Feb 26, 2024

True, I guess the API was never considered stable, which makes sense if it has to match an evolving language.
The phrasing there was "API might change slightly as a result of user feedback", which is far from "this doesn't work". And the comment should have been removed if such changes didn't appear soon after.

The API at the time was supported everywhere.
And the API has been stable, so the comment didn't end up meaning much.

Mirrors, as such, were not considered experimental in Dart 1. The concrete API was, possibly, conisdered a little experimental, but after surviving a few releases, that disclaimer didn't mean much.

@jakemac53
Copy link
Contributor

Like a reflectiveTest that finds all methods with a name starting with test, a macro could find the same methods, and create an allTests() method that calls them one by one.

Right, macros can definitely enable these class based test runners. It could pretty easily even just be a small layer on top of package:test, since tests/groups can be added programatically.

@gmpassos
Copy link
Contributor

gmpassos commented Feb 26, 2024

Macros will need to be able to inform the annotations of classes and methods or use them as filters and input parameters for generators.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-meta Cross-cutting, high-level issues (for tracking many other implementation issues, ...).
Projects
None yet
Development

No branches or pull requests