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

[flutter_local_notifications_windows] Windows FFI plugin #2366

Open
wants to merge 102 commits into
base: master
Choose a base branch
from

Conversation

Levi-Lesches
Copy link

@Levi-Lesches Levi-Lesches commented Jul 11, 2024

Update: I went in and simplified the code since yesterday, it's now in a reviewable state. If it would help, I can rebase + commit the first 50 or so commits and start with a cleaner slate.

This is a spin off of #2349, which implements the same logic but with an FFI plugin instead of method channels. I'm happy with it so far, here's the gist:

The C/C++ code

  • src/ffi_api.h: a C API between C++ and Dart
  • src/plugin.hpp: A C++ class that holds Windows-specific API handles
  • src/ffi_api.cpp: C++ code to implement the C API using the C++ class

The Dart code

  • a new package, flutter_local_notifications_windows that implements the same functionality as before.
  • lib/src/details holds all the platform-specific details
  • lib/src/ffi holds basic FFI stuff, like generated bindings and utils
  • lib/src/plugin holds the Windows plugin interface, the real FFI implementation, and a stub, because dart:ffi will break web apps (note, the API does reference dart:io but it still compiles on web)

Notes

  • Overall, the only thing that changed was how to go from Dart <--> C++. Before, it was all JSON-based message passing, and a big HandleMethodCall function that had to handle every possibility. I'm glad that's gone. Now we have a similar mechanism with Dart <--> C <--> C++ and package:ffigen sets everything up for us.
  • It's all backed with compile-time safety as opposed to the runtime failures I was facing with method channels. For example, passing a string now looks like "hello".toNativeString(), but other types require allocating memory manually.
  • Functionally, everything is working how it was before, with the method channels

Benefits

  • the C++ code is ~150 lines smaller, and went from being one big 250 line file to two 150 line files, one of which can be skimmed as it's just Windows registry config.
  • The C++ code no longer uses method channels or any of that associated logic. There's a lot of magic there involving serialization which also comes with a runtime cost. The new code directly passes values and pointers from Dart to C++, eliminating all the message passing. In fact, the new API is inherently synchronous instead of async (not that it matters in this context, since the other platforms are still async).
  • The Windows implementation now lives in flutter_local_notifications_windows instead of hijacking the original package and half-sharing some of the original implementations. That means no more platform checks, all the logic can be safely dealt with in a Windows-only context.

Summary of changes

  • Windows C/C++ code: +600 lines
  • Windows Dart API for customizing notification details): +800 lines
  • Windows Dart plugin code, excluding generated code: +300 lines
  • Main plugin integrations: < 50 lines
  • Example Dart code: +500 lines

That's only ~2,200 additions. the other half GitHub is reporting is automatically generated, like the example's new windows folder.

Important

FFI is a more recent development than method channels. As such, some more advanced FFI features are locked behind some more recent Dart versions. I already had to downgrade package:ffigen from a beta 13.0 to a whopping 7.0, but more pressing is that for C to call a Dart function from another thread (eg, when a notification is pressed), you need to use features from Dart 3.1. The current constraint is only 2.17.

Including this new implementation will force Dart ^3.1.0 on end-users. At this point, 2.17 is two years old, 3.1 is about one year old, and null safety is almost 3.5 years old. Age aside, I also don't believe going from 2.17 to 3.1 is so burdensome, especially since 2.17 is already after null-safety, and the Dart team's official position is that 3.0 is not really a breaking change in practice. In other words, I'm not sure I see a situation where someone is really stuck. They can either upgrade any pre-null-safety code, not upgrade this package further, or fork this package to add null Windows or future updates as needed.

Overall, I'd recommend bumping the SDK version of the front-facing package to 3.1.0 as well to better advertise this, and to benefit from any features, since 2.17 and beyond -- better null promotion, FFI for mobile platforms, class modifiers, macros, and more.

@MaikuB
Copy link
Owner

MaikuB commented Aug 24, 2024

Thanks for making the changes. I left one comment. Other thing was I wasn't too sure if you were done addressing the visibility of methods to with XML conversion. Since you mentioned it's good to go then FYI that there are still some methods that are publicly visible. I've commented on the parts that I could see are still visible to be more explicit. Apologies for spam on that but though it help make them easier to find. FYI that the way to verify is to follow the steps here to generate the API docs for the Windows plugin.

Just noticed that there's also errors on the dependency/version conflict that wasn't there before. Presumably ffigen got bumped that it's no longer compatible with what was stated before on Dart 3.1 being the minimum

Edit: when I checked the API docs for the Windows plugin and Linux one, I've noticed an existing problem where the docs for the stub is what is shown. Not a showstopper for the PR right now as it's an existing problem already that I've not seen anyone raise with the Linux implementation. Not sure how to solve this one but open to ideas. Off the top of my head, I suspect either the stub needs to have the same API docs that would be a bit of a hack or see if there's a way that only the real FlutterLocalNotificationsWindows class exists. This would delegates all the API calls to another class and conditional import logic based on support FFI determines if said class is a stub or real one. I've created a PR for fixing the Linux one that can be used to see what I mean. Be keen to know what you think. I've verified this by temporarily adding a web target to the example and confirmed that running flutter build web worked

@Levi-Lesches
Copy link
Author

when I checked the API docs for the Windows plugin and Linux one, I've noticed an existing problem where the docs for the stub is what is shown. Not a showstopper for the PR right now as it's an existing problem already that I've not seen anyone raise with the Linux implementation. Not sure how to solve this one but open to ideas.

When a subclass overrides a member, that member inherits its docs from the overriden version in the parent class, so long as docs are not explicitly provided on the override itself. This seems to be why the Linux version shows the stub docs -- the stub's overrides all have doc comments. To fix this, we should just not provide documentation on the stub class and let in inherit directly from the base class.

That just leaves the documentation on the class itself. I think the simplest fix would then be to copy the base class's documentation onto the stub class, which is a lot less work than copying every single member. So now:

  • all three classes (base, stub, and FFI) all have the same one-liner on the class declaration itself
  • any time a method is overriden, docs are not provided, and are instead inherited from the parent class
  • the plugin class shown on the docs site is the stub class, with comments from the base class
  • there is no way to get DartDoc to show the FFI code snippets when using conditional imports, only the stub snippets

@Levi-Lesches
Copy link
Author

Levi-Lesches commented Aug 25, 2024

Just noticed that there's also errors on the dependency/version conflict that wasn't there before. Presumably ffigen got bumped that it's no longer compatible with what was stated before on Dart 3.1 being the minimum

Yup, from package:ffigen v10.0.0 (currently at 13.0.0):

Stable release targeting Dart 3.2 using new dart:ffi features available in Dart 3.2 and later.

Those specific features aren't used by us in this exact case, but the features from ffigen 10.0 -- 13.0 (soon to be 14.0) will be, especially exhaustive enum checking. I think it would be worth bumping the minimum SDK version to 3.2. Now that we've passed 3.0, I'm not sure I see much risk in bumping the minor versions, and most other packages tend to stay current with the latest Dart releases. I bumped it in my latest commit so it should work now

@Levi-Lesches
Copy link
Author

Thanks for making the changes. I left one comment. Other thing was I wasn't too sure if you were done addressing the visibility of methods to with XML conversion. Since you mentioned it's good to go then FYI that there are still some methods that are publicly visible. I've commented on the parts that I could see are still visible to be more explicit. Apologies for spam on that but though it help make them easier to find. FYI that the way to verify is to follow the steps here to generate the API docs for the Windows plugin.

I actually figured it would probably be more convenient to leave them public. Since we offer a way to show notifications as raw XML, this would allow developers to build that XML using elements from the public API. Let's say they want to use their own <image>, they can still build the rest of the notification XML using .toXml() of everything else. It's not so important to me but I figured it would only reduce duplicate work and make custom XML easier. I'm not worried about stability or breaking changes because these methods are anyway compliant with the official Windows APIs which aren't breaking anytime soon.

If we really wanted to keep everything private, I could move them all to extensions, and then hide them in the exports section. But I feel like that's going to extra lengths to prevent possibly useful code from being used or seen. Not to mention, having the code snippets available in the docs site would also be useful to those looking to build their own XML.

Regarding the naming convention, I switched everything to .buildXml()

- Changed `toXml` to `buildXml`
- Fixed comments on stub class
- Bumped Dart version to ^3.2
- Fixed `enableMultithreading` not being always annotated
- Only test on Windows
@MaikuB
Copy link
Owner

MaikuB commented Aug 26, 2024

When a subclass overrides a member, that member inherits its docs from the overriden version in the parent class, so long as docs are not explicitly provided on the override itself.

Good callout as I admittedly forgot about that. Works for me and will update the approach used for Linux as well. Thanks!

Those specific features aren't used by us in this exact case, but the features from ffigen 10.0 -- 13.0 (soon to be 14.0) will be, especially exhaustive enum checking. I think it would be worth bumping the minimum SDK version to 3.2. Now that we've passed 3.0, I'm not sure I see much risk in bumping the minor versions, and most other packages tend to stay current with the latest Dart releases. I bumped it in my latest commit so it should work now

Thanks though building for 3.13 will fail as the main plugin itself has a minimum of 3.13 so that and pipeline would need updating as part of the PR too. I've withheld bumping for this plugin for a while and would still need to be careful of it in the future as it could affect the minimum supported OS versions

If we really wanted to keep everything private, I could move them all to extensions, and then hide them in the exports section

Yep would like to keep these hidden. These methods depend on an existing object passed through so AFAICT there's some prerequisites on what needs to be done before calling these build methods. When I was referring to breaking changes, it was more around how if these methods are part of the public API surface, updates to them (e.g. renaming) are considered breaking changes. The hiding is to help reduce the burden on a maintenance perspective. As for your point on hide, if all the extensions were moved to notification_to_xml.dart then they will be hidden without extra code. This is because the approach to determine what is publicly visible is done by determining which implementation files in the src folder are exported

@Levi-Lesches
Copy link
Author

Thanks though building for 3.13 will fail as the main plugin itself has a minimum of 3.13 so that and pipeline would need updating as part of the PR too. I've withheld bumping for this plugin for a while and would still need to be careful of it in the future as it could affect the minimum supported OS versions

Bumped pubpsec.yaml and GitHub workflow environments to use Dart 3.2 and Flutter 3.16 (which comes with Dart 3.2)

Yep would like to keep these hidden.

Got it, moved all that to extensions in lib/src/details/xml that are not exposed publicly. It caused an issue in two cases where I use subclasses, since making buildXml a member of the parent class would make it public. I tried using @internal from package:meta, but then I decided to seal their abstract classes and use a switch case over the subtypes.


On another note, I restricted the tests to run on Windows only, but the current workflows are running on ubuntu-latest. I'll leave that to you to handle since I think Windows CI is billed at a 2x rate compared to the Linux CI. But at least now, dart test on ubuntu-latest will skip the Windows tests automatically, and we can run the tests locally for now. I'm actually not even sure how notification privileges will work on GitHub servers. I don't see a specific issue, but it sounds like one of those things that could work differently compared to, say, my laptop.

@abdelaziz-mahdy
Copy link

Thanks though building for 3.13 will fail as the main plugin itself has a minimum of 3.13 so that and pipeline would need updating as part of the PR too. I've withheld bumping for this plugin for a while and would still need to be careful of it in the future as it could affect the minimum supported OS versions

Bumped pubpsec.yaml and GitHub workflow environments to use Dart 3.2 and Flutter 3.16 (which comes with Dart 3.2)

Yep would like to keep these hidden.

Got it, moved all that to extensions in lib/src/details/xml that are not exposed publicly. It caused an issue in two cases where I use subclasses, since making buildXml a member of the parent class would make it public. I tried using @internal from package:meta, but then I decided to seal their abstract classes and use a switch case over the subtypes.

On another note, I restricted the tests to run on Windows only, but the current workflows are running on ubuntu-latest. I'll leave that to you to handle since I think Windows CI is billed at a 2x rate compared to the Linux CI. But at least now, dart test on ubuntu-latest will skip the Windows tests automatically, and we can run the tests locally for now. I'm actually not even sure how notification privileges will work on GitHub servers. I don't see a specific issue, but it sounds like one of those things that could work differently compared to, say, my laptop.

i would like to note that public repos actions are free, so i think the rate is not a problem? https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#about-billing-for-github-actions

@MaikuB
Copy link
Owner

MaikuB commented Sep 4, 2024

@Levi-Lesches sorry for late response, was recovering from surgery. It looks like there's still a conflict. Based on my understanding of the constraints set by ffigen 13.0, this actually needs Flutter 3.19/Dart 3.3 (reference: table under stable at https://docs.flutter.dev/release/archive). Are you able to help resolve this

I'm actually not even sure how notification privileges will work on GitHub servers.

I could be mistaken but presumably as they're unit tests, the priviledges to be concerned about. Given what @abdelaziz-mahdy has said, you think you could push an update to have the tests run on Windows? On a related note, is it possible to make updates so that so that the example app is built on Windows as part of the GitHub workflow too? Would require updates to melos.yaml and validate.yml

I've also noticed there's a lot of files with changes picked up if I run dart format . from the root of the repo. Are you able to run that (or melos run format) and push the changes? I'll look into how to make this smoother separately in another PR to help others in the future (e.g. see if the workflow can help catch this)

Edit: I've merged in #2398 that should help highlight formatting issues in the future

@Levi-Lesches
Copy link
Author

was recovering from surgery

Sorry to hear, hope that's going well 🙂

Based on my understanding of the constraints set by ffigen 13.0, this actually needs Flutter 3.19/Dart 3.3 (reference: table under stable at https://docs.flutter.dev/release/archive). Are you able to help resolve this

That's annoying, I checked the changelog and found mention of 3.2 but not 3.3! I changed everything to Dart 3.3 and Flutter 3.19 now.

I could be mistaken but presumably as they're unit tests, the priviledges to be concerned about.

I didn't mock everything out, as I was interested in maintaining tests that the XML does in fact conform to the Windows API. So the unit tests actually do send full XML to the Windows API and try to send notifications. If this doesn't work on the server, I'd still like to keep them, but maybe we'll just run them manually when changing XML code and configure the CI to only run "safe" tests. Speaking of, I added a global retry: 5 to all the tests, given that there are some concurrency issues.

you think you could push an update to have the tests run on Windows? On a related note, is it possible to make updates so that so that the example app is built on Windows as part of the GitHub workflow too? Would require updates to melos.yaml and validate.yml

Hit an annoying issue where "skip these tests on Windows" was actually causing Linux tests to fail. Filed dart-lang/test#2277 to propose to change that, but in the meantime I exclude _windows in the standard unit tests and run it separately as test:unit:windows, only on windows-latest. Needs another workflow approval.

I've also noticed there's a lot of files with changes picked up if I run dart format . from the root of the repo. Are you able to run that (or melos run format) and push the changes?

Done and pushed


On another note, I had previously tried to make the Dart package completely separate from Flutter so it can be used in CLI apps. I still believe there is value to that, but when running dart pub lish -n we get an error that is never an issue in practice:

Package validation found the following error:
* pubspec.yaml allows Flutter SDK version 1.9.x, which does not support the flutter.plugin.platforms key.
  Please consider increasing the Flutter SDK requirement to ^1.10.0 (environment.sdk.flutter)

  See https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin

Basically, even though having a flutter: section in the Pubspec of a non-Flutter package is harmless, theoretically, someone can use a very early version of Flutter (1.9.x) which would have issues parsing this section. Really, this can never happen because we demand an sdk: ^3.3 and Flutter 1.9 is pre null-safety. So Pub wants us to demand flutter: ^1.10.0, but that means declaring a dependency on Flutter at all, which I was really hoping to avoid. See also this issue.

My recommendation is to run dart pub lish -n first, see if there are any warnings besides the above, and address them first. Then, once this is the only warning left, running dart pub lish --skip-validation to ignore the warning, and preserve CLI ability.

@MaikuB
Copy link
Owner

MaikuB commented Sep 5, 2024

I'll take a look and come back to you on these but thought I'd reply in case you missed it since it wasn't brought up in your last message and mention that builds are failing due to the meta version specified

@Levi-Lesches
Copy link
Author

Good eye -- fixed

@MaikuB
Copy link
Owner

MaikuB commented Sep 6, 2024

Sorry to hear, hope that's going well 🙂

Thanks minor surgery and still bit of discomfort but all good besides that :)

On another note, I had previously tried to make the Dart package completely separate from Flutter so it can be used in CLI apps.

I forgot to ask but is this realistically possible? In the case of the platform interface, this would normally be a transitive dependency. In the case of the Windows plugin, I don't know if you could build a CLI app that shows notifications on the Windows platform? Presumably what you mean is the equivalent of using Visual Studio to create a console app and referencing the Windows APIs to display notifications

My recommendation is to run dart pub lish -n first, see if there are any warnings besides the above, and address them first

This is what I get besides the Flutter one

    error - lib/src/plugin/ffi.dart:122:7 - The named parameter 'data' isn't defined. Try correcting the name to an existing named parameter's name, or defining a named parameter with the name 'data'. - undefined_named_parameter
    error - lib/src/plugin/ffi.dart:200:9 - The named parameter 'data' isn't defined. Try correcting the name to an existing named parameter's name, or defining a named parameter with the name 'data'. - undefined_named_parameter

I've taken a look at the issue you linked and conscious it's been open for a while. I do have a number of thoughts relating to this to and may be missing some additional context and other information so do let me know if I'm missing anything. If issue linked addressed and there are valid cases for CLI apps, I wonder if it'd be better for the "greater good" to go ahead with the Flutter requirement and revisit this. Those who use this plugin would be those looking to build a multiplatform apps. Other aspect of this is if the issue you raised is addressed, then this presumably requires newer version of the Dart SDK. If so, this would cause the Flutter SDK requirement to go even higher and limit the reach even further.

If it's possible to build a Dart CLI app that can show notifications on Windows then this sounds like it one target a very small audience if such an audience even exists. I'm basing this on the assumption that this implies that this is a developer who works on the Windows platform and rather than building a Windows console application, has decided to use Dart. Is this what it would mean? If so, this seems like an odd choice to me. In my experience, companies that build dedicated solutions targeting Windows would employ devs and leverage based on .NET

On a different note, it looks like Windows example is failing to built I'm not sure what's the cause there. However, I'm noticing that the integration tests are failing to run for Android. It seems as though the deprecation warning is being treated as a error but can't reproduce this locally even if I run the integration tests via melos. Given I had raised a PR recently to update one of the workflows and didn't have this issue, I wonder if something got changed that impacted this?

@MaikuB
Copy link
Owner

MaikuB commented Sep 14, 2024

It's been a while since I've worked on Windows apps so didn't get to spend much time on it but ran into the following problems relating my previous post

  • I had trouble getting a console app to create a notification. I was hoping to copy some of the code for the Windows App SDK samples for showing notifications to achieve this but trouble with having appropriate namespaces and NuGet packages resolve properly
  • I tried to create Dart CLI app by creating a separate folder in the cloned repo, reference only the Windows plugin, bootstrap melos etc. Upon trying to run the CLI app, I saw Error: Dart library 'dart:ui' is not available on this platform. as an error message. It looks like this happens as the dependencies the plugin eventually need it and that's only available for apps that depends on Flutter

Based on the above it looks like trying to support CLI apps isn't actually possible, at least not in the current state either

Comment on lines +221 to +225
PWSTR fullName = (PWSTR) malloc(length * sizeof(*fullName));
if (fullName == nullptr) return std::nullopt;
error = GetCurrentPackageFullName(&length, fullName);
if (error != ERROR_SUCCESS) return std::nullopt;
free(fullName);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a std::vector. You currently have a memory leak in the case of an error.

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

Successfully merging this pull request may close these issues.

9 participants