diff --git a/Cargo.lock b/Cargo.lock index e6942ef72..5c4621f57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1476,9 +1476,7 @@ dependencies = [ [[package]] name = "flutter_rust_bridge_codegen" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3209735fd687b06b8d770ec008874119b91f7f46b4a73d17226d5c337435bb74" +version = "1.32.0" dependencies = [ "anyhow", "cargo_metadata", diff --git a/Cargo.toml b/Cargo.toml index 2b707a688..f046df244 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,7 +105,7 @@ jni = "0.19.0" flutter_rust_bridge = "1.30.0" [workspace] -members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display"] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/flutter_rust_bridge_codegen"] [package.metadata.winres] LegalCopyright = "Copyright © 2022 Purslane, Inc." @@ -119,7 +119,7 @@ winapi = { version = "0.3", features = [ "winnt" ] } [build-dependencies] cc = "1.0" hbb_common = { path = "libs/hbb_common" } -flutter_rust_bridge_codegen = "1.30.0" +flutter_rust_bridge_codegen = { path = "libs/flutter_rust_bridge_codegen" } [dev-dependencies] hound = "3.4" diff --git a/flutter/.gitignore b/flutter/.gitignore index 7dc95a613..c8ff34feb 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -45,6 +45,7 @@ jniLibs # flutter rust bridge lib/generated_bridge.dart +lib/generated_bridge.freezed.dart # Flutter Generated Files linux/flutter/generated_plugin_registrant.cc diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 4eaa1c877..1ba610f19 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1,6 +1,20 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "40.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" archive: dependency: transitive description: @@ -64,6 +78,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.11" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.3" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.3.2" characters: dependency: transitive description: @@ -78,6 +148,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" clock: dependency: transitive description: @@ -85,6 +162,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" collection: dependency: transitive description: @@ -92,6 +176,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" cross_file: dependency: transitive description: @@ -113,6 +204,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" dash_chat: dependency: "direct main" description: @@ -319,6 +417,41 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3+1" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" http: dependency: "direct main" description: @@ -326,6 +459,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.4" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" http_parser: dependency: transitive description: @@ -382,6 +522,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" js: dependency: transitive description: @@ -389,6 +536,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" matcher: dependency: transitive description: @@ -410,6 +571,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" nested: dependency: transitive description: @@ -417,6 +585,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" package_info_plus: dependency: "direct main" description: @@ -543,6 +718,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" process: dependency: transitive description: @@ -557,6 +739,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.0.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" qr_code_scanner: dependency: "direct main" description: @@ -636,11 +832,32 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" source_span: dependency: transitive description: @@ -662,6 +879,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" string_scanner: dependency: transitive description: @@ -683,6 +907,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.9" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" toggle_switch: dependency: "direct main" description: @@ -816,6 +1047,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" win32: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 008d4ef9d..21b1857eb 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -3,7 +3,7 @@ description: Your Remote Desktop Software # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -19,102 +19,102 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.1.10+27 environment: - sdk: ">=2.16.1" + sdk: ">=2.16.1" dependencies: - flutter: - sdk: flutter + flutter: + sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.3 - ffi: ^1.1.2 - path_provider: ^2.0.2 - external_path: ^1.0.1 - provider: ^5.0.0 - tuple: ^2.0.0 - wakelock: ^0.5.2 - device_info_plus: ^3.2.3 - firebase_analytics: ^9.1.5 - package_info_plus: ^1.4.2 - url_launcher: ^6.0.9 - shared_preferences: ^2.0.6 - toggle_switch: ^1.4.0 - dash_chat: ^1.1.16 - draggable_float_widget: ^0.0.2 - settings_ui: ^2.0.2 - flutter_breadcrumb: ^1.0.1 - http: ^0.13.4 - qr_code_scanner: - git: - url: https://github.com/Heap-Hop/qr_code_scanner.git - ref: fix_break_changes_platform - zxing2: ^0.1.0 - image_picker: ^0.8.5 - image: ^3.1.3 - flutter_smart_dialog: ^4.3.1 - flutter_rust_bridge: ^1.30.0 - window_manager: ^0.2.3 - desktop_multi_window: - git: - url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: master - bitsdojo_window: ^0.1.2 + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.3 + ffi: ^1.1.2 + path_provider: ^2.0.2 + external_path: ^1.0.1 + provider: ^5.0.0 + tuple: ^2.0.0 + wakelock: ^0.5.2 + device_info_plus: ^3.2.3 + firebase_analytics: ^9.1.5 + package_info_plus: ^1.4.2 + url_launcher: ^6.0.9 + shared_preferences: ^2.0.6 + toggle_switch: ^1.4.0 + dash_chat: ^1.1.16 + draggable_float_widget: ^0.0.2 + settings_ui: ^2.0.2 + flutter_breadcrumb: ^1.0.1 + http: ^0.13.4 + qr_code_scanner: + git: + url: https://github.com/Heap-Hop/qr_code_scanner.git + ref: fix_break_changes_platform + zxing2: ^0.1.0 + image_picker: ^0.8.5 + image: ^3.1.3 + flutter_smart_dialog: ^4.3.1 + flutter_rust_bridge: ^1.30.0 + window_manager: ^0.2.3 + desktop_multi_window: + git: + url: https://github.com/Kingtous/rustdesk_desktop_multi_window + ref: master + bitsdojo_window: ^0.1.2 + freezed_annotation: ^2.0.3 dev_dependencies: - flutter_launcher_icons: ^0.9.1 - flutter_test: - sdk: flutter - + flutter_launcher_icons: ^0.9.1 + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + freezed: ^2.0.3 # rerun: flutter pub run flutter_launcher_icons:main flutter_icons: - android: "ic_launcher" - ios: true - image_path: "../1024-rec.png" + android: "ic_launcher" + ios: true + image_path: "../1024-rec.png" # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true + # To add assets to your application, add an assets section, like this: + assets: + - assets/ - # To add assets to your application, add an assets section, like this: - assets: - - assets/ + fonts: + - family: GestureIcons + fonts: + - asset: assets/gestures.ttf - fonts: - - family: GestureIcons - fonts: - - asset: assets/gestures.ttf + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/libs/flutter_rust_bridge_codegen/.gitignore b/libs/flutter_rust_bridge_codegen/.gitignore new file mode 100644 index 000000000..6985cf1bd --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/libs/flutter_rust_bridge_codegen/Cargo.toml b/libs/flutter_rust_bridge_codegen/Cargo.toml new file mode 100644 index 000000000..dfd1556db --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "flutter_rust_bridge_codegen" +version = "1.32.0" +edition = "2018" +description = "High-level memory-safe bindgen for Dart/Flutter <-> Rust" +license = "MIT" +repository = "https://github.com/fzyzcjy/flutter_rust_bridge" +keywords = ["flutter", "dart", "ffi", "code-generation", "bindings"] +categories = ["development-tools::ffi"] + +[lib] +name = "lib_flutter_rust_bridge_codegen" +path = "src/lib.rs" + +[[bin]] +name = "flutter_rust_bridge_codegen" +path = "src/main.rs" + +[dependencies] +syn = { version = "1.0.77", features = ["full", "extra-traits"] } +quote = "1.0" +regex = "1.5.4" +lazy_static = "1.4.0" +convert_case = "0.5.0" +tempfile = "3.2.0" +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.8" +log = "0.4" +env_logger = "0.9.0" +structopt = "0.3" +toml = "0.5.8" +anyhow = "1.0.44" +pathdiff = "0.2.1" +cargo_metadata = "0.14.1" +enum_dispatch = "0.3.8" +thiserror = "1" +cbindgen = "0.23" \ No newline at end of file diff --git a/libs/flutter_rust_bridge_codegen/README.md b/libs/flutter_rust_bridge_codegen/README.md new file mode 100644 index 000000000..d9aa76531 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/README.md @@ -0,0 +1,95 @@ +# [flutter_rust_bridge](https://github.com/fzyzcjy/flutter_rust_bridge): High-level memory-safe binding generator for Flutter/Dart <-> Rust + +[![Rust Package](https://img.shields.io/crates/v/flutter_rust_bridge.svg)](https://crates.io/crates/flutter_rust_bridge) +[![Flutter Package](https://img.shields.io/pub/v/flutter_rust_bridge.svg)](https://pub.dev/packages/flutter_rust_bridge) +[![Stars](https://img.shields.io/github/stars/fzyzcjy/flutter_rust_bridge)](https://github.com/fzyzcjy/flutter_rust_bridge) +[![CI](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/ci.yaml/badge.svg)](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/ci.yaml) +[![Example](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/post_release.yaml/badge.svg)](https://github.com/fzyzcjy/flutter_rust_bridge/actions/workflows/post_release.yaml) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/6afbdad19e7245adbf9e9771777be3d7)](https://app.codacy.com/gh/fzyzcjy/flutter_rust_bridge?utm_source=github.com&utm_medium=referral&utm_content=fzyzcjy/flutter_rust_bridge&utm_campaign=Badge_Grade_Settings) + +![Logo](https://github.com/fzyzcjy/flutter_rust_bridge/raw/master/book/logo.png) + +Want to combine the best between [Flutter](https://flutter.dev/), a cross-platform hot-reload rapid-development UI toolkit, and [Rust](https://www.rust-lang.org/), a language empowering everyone to build reliable and efficient software? Here it comes! + +## 🚀 Advantages + +* **Memory-safe**: Never need to think about malloc/free. +* **Feature-rich**: `enum`s with values, platform-optimized `Vec`, possibly recursive `struct`, zero-copy big arrays, `Stream` (iterator) abstraction, error (`Result`) handling, cancellable tasks, concurrency control, and more. See full features [here](https://fzyzcjy.github.io/flutter_rust_bridge/feature.html). +* **Async programming**: Rust code will never block the Flutter. Call Rust naturally from Flutter's main isolate (thread). +* **Lightweight**: This is not a huge framework that includes everything, so you are free to use your favorite Flutter and Rust libraries. For example, state-management with Flutter library (e.g. MobX) can be elegant and simple (contrary to implementing in Rust); implementing a photo manipulation algorithm in Rust will be fast and safe (countrary to implementing in Flutter). +* **Cross-platform**: Android, iOS, Windows, Linux, MacOS ([Web](https://github.com/fzyzcjy/flutter_rust_bridge/issues/315) coming soon) +* **Easy to code-review & convince yourself**: This package simply simulates how humans write boilerplate code. If you want to convince yourself (or your team) that it is safe, there is not much code to look at. No magic at all! ([More about](https://fzyzcjy.github.io/flutter_rust_bridge/safety.html) safety concerns.) +* **Fast**: It is only a thin (though feature-rich) wrapper, without overhead such as protobuf serialization, thus performant. (More [benchmarks](https://github.com/fzyzcjy/flutter_rust_bridge/issues/318#issuecomment-1034536815) later) (Throw away components like thread-pool to make it even faster) +* **Pure-Dart compatible:** Despite the name, this package is 100% compatible with [pure](https://github.com/fzyzcjy/flutter_rust_bridge/blob/master/frb_example/pure_dart/README.md) Dart. + +## 💡 User Guide + +Check out [the user guide](https://fzyzcjy.github.io/flutter_rust_bridge/) for [show-me-the-code](https://fzyzcjy.github.io/flutter_rust_bridge/quickstart.html), [tutorials](https://fzyzcjy.github.io/flutter_rust_bridge/tutorial_with_flutter.html), [features](https://fzyzcjy.github.io/flutter_rust_bridge/feature.html) and much more. + +## 📎 P.S. Convenient Flutter tests + +If you want to write and debug tests in Flutter conveniently, with action history, time travelling, screenshots, rapid re-execution, video recordings, interactive mode and more, here is my another open-source library: https://github.com/fzyzcjy/flutter_convenient_test. + +## ✨ Contributors + + +[![All Contributors](https://img.shields.io/badge/all_contributors-18-orange.svg?style=flat-square)](#contributors-) + + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key) following [all-contributors](https://github.com/all-contributors/all-contributors) specification): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

fzyzcjy

💻 📖 💡 🤔 🚧

Viet Dinh

💻 ⚠️ 📖

Joshua Wade

💻

Marcel

💻

rustui

📖

Michael Bryan

💻

bus710

📖

Sebastian Urban

💻

Daniel

💻

Kevin Li

💻 📖

Patrick Auernig

💻

Anton Lazarev

💻

Unoqwy

💻

Febrian Setianto

📖

syndim

💻

sagu

💻 📖

Ikko Ashimine

📖

alanlzhang

💻 📖
+ + + + + + +More specifically, thanks for all these contributions: + +* [Desdaemon](https://github.com/Desdaemon): Support not only simple enums but also enums with fields which gets translated to native enum or freezed class in Dart. Support the Option type as nullable types in Dart. Support Vec of Strings type. Support comments in code. Add marker attributes for future usage. Add Linux and Windows support for with-flutter example, and make CI works for that. Avoid parameter collision. Overhaul the documentation and add several chapters to demonstrate configuring a Flutter+Rust project in all five platforms. Refactor command module. +* [SecondFlight](https://github.com/SecondFlight): Allow structs and enums to be imported from other files within the crate by creating source graph. Auto-create relavent dir. +* [Unoqwy](https://github.com/Unoqwy): Add struct mirrors, such that types in the external crates can be imported and used without redefining and copying. +* [antonok-edm](https://github.com/antonok-edm): Avoid converting syn types to strings before parsing to improve code and be more robust. +* [sagudev](https://github.com/sagudev): Make code generator a `lib`. Add error types. Depend on `cbindgen`. Fix LLVM paths. Update deps. Fix CI errors. +* [surban](https://github.com/surban): Support unit return type. Skip unresolvable modules. Ignore prefer_const_constructors. Non-final Dart fields. +* [trobanga](https://github.com/trobanga): Add support for `[T;N]` structs. Add `usize` support. Add a cmd argument. Separate dart tests. +* [AlienKevin](https://github.com/AlienKevin): Add flutter example for macOS. Add doc for Android NDK bug. +* [alanlzhang](https://github.com/alanlzhang): Add generation for Dart metadata. +* [efc-mw](https://github.com/efc-mw): Improve Windows encoding handling. +* [valeth](https://github.com/valeth): Rename callFfi's port. +* [Michael-F-Bryan](https://github.com/Michael-F-Bryan): Detect broken bindings. +* [bus710](https://github.com/bus710): Add a case in troubleshooting. +* [Syndim](https://github.com/Syndim): Add a bracket to box. +* [feber](https://github.com/feber): Fix doc link. +* [rustui](https://github.com/rustui): Fix a typo. +* [eltociear](https://github.com/eltociear): Fix a typo. + diff --git a/libs/flutter_rust_bridge_codegen/src/commands.rs b/libs/flutter_rust_bridge_codegen/src/commands.rs new file mode 100644 index 000000000..6838449d8 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/commands.rs @@ -0,0 +1,267 @@ +use std::fmt::Write; +use std::path::Path; +use std::process::Command; +use std::process::Output; + +use crate::error::{Error, Result}; +use log::{debug, info, warn}; + +#[must_use] +fn call_shell(cmd: &str) -> Output { + #[cfg(windows)] + return execute_command("powershell", &["-noprofile", "-c", cmd], None); + + #[cfg(not(windows))] + execute_command("sh", &["-c", cmd], None) +} + +pub fn ensure_tools_available() -> Result { + let output = call_shell("dart pub global list"); + let output = String::from_utf8_lossy(&output.stdout); + if !output.contains("ffigen") { + return Err(Error::MissingExe(String::from("ffigen"))); + } + + Ok(()) +} + +pub fn bindgen_rust_to_dart( + rust_crate_dir: &str, + c_output_path: &str, + dart_output_path: &str, + dart_class_name: &str, + c_struct_names: Vec, + llvm_install_path: &[String], + llvm_compiler_opts: &str, +) -> anyhow::Result<()> { + cbindgen(rust_crate_dir, c_output_path, c_struct_names)?; + ffigen( + c_output_path, + dart_output_path, + dart_class_name, + llvm_install_path, + llvm_compiler_opts, + ) +} + +#[must_use = "Error path must be handled."] +fn execute_command(bin: &str, args: &[&str], current_dir: Option<&str>) -> Output { + let mut cmd = Command::new(bin); + cmd.args(args); + + if let Some(current_dir) = current_dir { + cmd.current_dir(current_dir); + } + + debug!( + "execute command: bin={} args={:?} current_dir={:?} cmd={:?}", + bin, args, current_dir, cmd + ); + + let result = cmd + .output() + .unwrap_or_else(|err| panic!("\"{}\" \"{}\" failed: {}", bin, args.join(" "), err)); + + let stdout = String::from_utf8_lossy(&result.stdout); + if result.status.success() { + debug!( + "command={:?} stdout={} stderr={}", + cmd, + stdout, + String::from_utf8_lossy(&result.stderr) + ); + if stdout.contains("fatal error") { + warn!("See keywords such as `error` in command output. Maybe there is a problem? command={:?} output={:?}", cmd, result); + } else if args.contains(&"ffigen") && stdout.contains("[SEVERE]") { + // HACK: If ffigen can't find a header file it will generate broken + // bindings but still exit successfully. We can detect these broken + // bindings by looking for a "[SEVERE]" log message. + // + // It may emit SEVERE log messages for non-fatal errors though, so + // we don't want to error out completely. + + warn!( + "The `ffigen` command emitted a SEVERE error. Maybe there is a problem? command={:?} output=\n{}", + cmd, String::from_utf8_lossy(&result.stdout) + ); + } + } else { + warn!( + "command={:?} stdout={} stderr={}", + cmd, + stdout, + String::from_utf8_lossy(&result.stderr) + ); + } + result +} + +fn cbindgen( + rust_crate_dir: &str, + c_output_path: &str, + c_struct_names: Vec, +) -> anyhow::Result<()> { + debug!( + "execute cbindgen rust_crate_dir={} c_output_path={}", + rust_crate_dir, c_output_path + ); + + let config = cbindgen::Config { + language: cbindgen::Language::C, + sys_includes: vec![ + "stdbool.h".to_string(), + "stdint.h".to_string(), + "stdlib.h".to_string(), + ], + no_includes: true, + export: cbindgen::ExportConfig { + include: c_struct_names + .iter() + .map(|name| format!("\"{}\"", name)) + .collect::>(), + ..Default::default() + }, + ..Default::default() + }; + + debug!("cbindgen config: {:?}", config); + + let canonical = Path::new(rust_crate_dir) + .canonicalize() + .expect("Could not canonicalize rust crate dir"); + let mut path = canonical.to_str().unwrap(); + + // on windows get rid of the UNC path + if path.starts_with(r"\\?\") { + path = &path[r"\\?\".len()..]; + } + + if cbindgen::generate_with_config(path, config)?.write_to_file(c_output_path) { + Ok(()) + } else { + Err(Error::str("cbindgen failed writing file").into()) + } +} + +fn ffigen( + c_path: &str, + dart_path: &str, + dart_class_name: &str, + llvm_path: &[String], + llvm_compiler_opts: &str, +) -> anyhow::Result<()> { + debug!( + "execute ffigen c_path={} dart_path={} llvm_path={:?}", + c_path, dart_path, llvm_path + ); + let mut config = format!( + " + output: '{}' + name: '{}' + description: 'generated by flutter_rust_bridge' + headers: + entry-points: + - '{}' + include-directives: + - '{}' + comments: false + preamble: | + // ignore_for_file: camel_case_types, non_constant_identifier_names, avoid_positional_boolean_parameters, annotate_overrides, constant_identifier_names + ", + dart_path, dart_class_name, c_path, c_path, + ); + if !llvm_path.is_empty() { + write!( + &mut config, + " + llvm-path:\n" + )?; + for path in llvm_path { + writeln!(&mut config, " - '{}'", path)?; + } + } + + if !llvm_compiler_opts.is_empty() { + config = format!( + "{} + compiler-opts: + - '{}'", + config, llvm_compiler_opts + ); + } + + debug!("ffigen config: {}", config); + + let mut config_file = tempfile::NamedTempFile::new()?; + std::io::Write::write_all(&mut config_file, config.as_bytes())?; + debug!("ffigen config_file: {:?}", config_file); + + // NOTE please install ffigen globally first: `dart pub global activate ffigen` + let res = call_shell(&format!( + "dart pub global run ffigen --config \"{}\"", + config_file.path().to_string_lossy() + )); + if !res.status.success() { + let err = String::from_utf8_lossy(&res.stderr); + let out = String::from_utf8_lossy(&res.stdout); + let pat = "Couldn't find dynamic library in default locations."; + if err.contains(pat) || out.contains(pat) { + return Err(Error::FfigenLlvm.into()); + } + return Err( + Error::string(format!("ffigen failed:\nstderr: {}\nstdout: {}", err, out)).into(), + ); + } + Ok(()) +} + +pub fn format_rust(path: &str) -> Result { + debug!("execute format_rust path={}", path); + let res = execute_command("rustfmt", &[path], None); + if !res.status.success() { + return Err(Error::Rustfmt( + String::from_utf8_lossy(&res.stderr).to_string(), + )); + } + Ok(()) +} + +pub fn format_dart(path: &str, line_length: i32) -> Result { + debug!( + "execute format_dart path={} line_length={}", + path, line_length + ); + let res = call_shell(&format!( + "dart format {} --line-length {}", + path, line_length + )); + if !res.status.success() { + return Err(Error::Dartfmt( + String::from_utf8_lossy(&res.stderr).to_string(), + )); + } + Ok(()) +} + +pub fn build_runner(dart_root: &str) -> Result { + info!("Running build_runner at {}", dart_root); + let out = if cfg!(windows) { + call_shell(&format!( + "cd \"{}\"; flutter pub run build_runner build --delete-conflicting-outputs", + dart_root + )) + } else { + call_shell(&format!( + "cd \"{}\" && flutter pub run build_runner build --delete-conflicting-outputs", + dart_root + )) + }; + if !out.status.success() { + return Err(Error::StringError(format!( + "Failed to run build_runner for {}: {}", + dart_root, + String::from_utf8_lossy(&out.stdout) + ))); + } + Ok(()) +} diff --git a/libs/flutter_rust_bridge_codegen/src/config.rs b/libs/flutter_rust_bridge_codegen/src/config.rs new file mode 100644 index 000000000..de77cd1b1 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/config.rs @@ -0,0 +1,292 @@ +use std::env; +use std::ffi::OsString; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::str::FromStr; + +use anyhow::{anyhow, Result}; +use convert_case::{Case, Casing}; +use serde::Deserialize; +use structopt::clap::AppSettings; +use structopt::StructOpt; +use toml::Value; + +#[derive(StructOpt, Debug, PartialEq, Deserialize, Default)] +#[structopt(setting(AppSettings::DeriveDisplayOrder))] +pub struct RawOpts { + /// Path of input Rust code + #[structopt(short, long)] + pub rust_input: String, + /// Path of output generated Dart code + #[structopt(short, long)] + pub dart_output: String, + /// If provided, generated Dart declaration code to this separate file + #[structopt(long)] + pub dart_decl_output: Option, + + /// Path of output generated C header + #[structopt(short, long)] + pub c_output: Option>, + /// Crate directory for your Rust project + #[structopt(long)] + pub rust_crate_dir: Option, + /// Path of output generated Rust code + #[structopt(long)] + pub rust_output: Option, + /// Generated class name + #[structopt(long)] + pub class_name: Option, + /// Line length for dart formatting + #[structopt(long)] + pub dart_format_line_length: Option, + /// Skip automatically adding `mod bridge_generated;` to `lib.rs` + #[structopt(long)] + pub skip_add_mod_to_lib: bool, + /// Path to the installed LLVM + #[structopt(long)] + pub llvm_path: Option>, + /// LLVM compiler opts + #[structopt(long)] + pub llvm_compiler_opts: Option, + /// Path to root of Dart project, otherwise inferred from --dart-output + #[structopt(long)] + pub dart_root: Option, + /// Skip running build_runner even when codegen-capable code is detected + #[structopt(long)] + pub no_build_runner: bool, + /// Show debug messages. + #[structopt(short, long)] + pub verbose: bool, +} + +#[derive(Debug)] +pub struct Opts { + pub rust_input_path: String, + pub dart_output_path: String, + pub dart_decl_output_path: Option, + pub c_output_path: Vec, + pub rust_crate_dir: String, + pub rust_output_path: String, + pub class_name: String, + pub dart_format_line_length: i32, + pub skip_add_mod_to_lib: bool, + pub llvm_path: Vec, + pub llvm_compiler_opts: String, + pub manifest_path: String, + pub dart_root: Option, + pub build_runner: bool, +} + +pub fn parse(raw: RawOpts) -> Opts { + let rust_input_path = canon_path(&raw.rust_input); + + let rust_crate_dir = canon_path(&raw.rust_crate_dir.unwrap_or_else(|| { + fallback_rust_crate_dir(&rust_input_path) + .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("rust_crate_dir"))) + })); + let manifest_path = { + let mut path = std::path::PathBuf::from_str(&rust_crate_dir).unwrap(); + path.push("Cargo.toml"); + path_to_string(path).unwrap() + }; + let rust_output_path = canon_path(&raw.rust_output.unwrap_or_else(|| { + fallback_rust_output_path(&rust_input_path) + .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("rust_output"))) + })); + let class_name = raw.class_name.unwrap_or_else(|| { + fallback_class_name(&*rust_crate_dir) + .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("class_name"))) + }); + let c_output_path = raw + .c_output + .map(|outputs| { + outputs + .iter() + .map(|output| canon_path(output)) + .collect::>() + }) + .unwrap_or_else(|| { + vec![fallback_c_output_path() + .unwrap_or_else(|_| panic!("{}", format_fail_to_guess_error("c_output")))] + }); + + let dart_root = { + let dart_output = &raw.dart_output; + raw.dart_root + .as_deref() + .map(canon_path) + .or_else(|| fallback_dart_root(dart_output).ok()) + }; + + Opts { + rust_input_path, + dart_output_path: canon_path(&raw.dart_output), + dart_decl_output_path: raw + .dart_decl_output + .as_ref() + .map(|s| canon_path(s.as_str())), + c_output_path, + rust_crate_dir, + rust_output_path, + class_name, + dart_format_line_length: raw.dart_format_line_length.unwrap_or(80), + skip_add_mod_to_lib: raw.skip_add_mod_to_lib, + llvm_path: raw.llvm_path.unwrap_or_else(|| { + vec![ + "/opt/homebrew/opt/llvm".to_owned(), // Homebrew root + "/usr/local/opt/llvm".to_owned(), // Homebrew x86-64 root + // Possible Linux LLVM roots + "/usr/lib/llvm-9".to_owned(), + "/usr/lib/llvm-10".to_owned(), + "/usr/lib/llvm-11".to_owned(), + "/usr/lib/llvm-12".to_owned(), + "/usr/lib/llvm-13".to_owned(), + "/usr/lib/llvm-14".to_owned(), + "/usr/lib/".to_owned(), + "/usr/lib64/".to_owned(), + "C:/Program Files/llvm".to_owned(), // Default on Windows + "C:/Program Files/LLVM".to_owned(), + "C:/msys64/mingw64".to_owned(), // https://packages.msys2.org/package/mingw-w64-x86_64-clang + ] + }), + llvm_compiler_opts: raw.llvm_compiler_opts.unwrap_or_else(|| "".to_string()), + manifest_path, + dart_root, + build_runner: !raw.no_build_runner, + } +} + +fn format_fail_to_guess_error(name: &str) -> String { + format!( + "fail to guess {}, please specify it manually in command line arguments", + name + ) +} + +fn fallback_rust_crate_dir(rust_input_path: &str) -> Result { + let mut dir_curr = Path::new(rust_input_path) + .parent() + .ok_or_else(|| anyhow!(""))?; + + loop { + let path_cargo_toml = dir_curr.join("Cargo.toml"); + + if path_cargo_toml.exists() { + return Ok(dir_curr + .as_os_str() + .to_str() + .ok_or_else(|| anyhow!(""))? + .to_string()); + } + + if let Some(next_parent) = dir_curr.parent() { + dir_curr = next_parent; + } else { + break; + } + } + Err(anyhow!( + "look at parent directories but none contains Cargo.toml" + )) +} + +fn fallback_c_output_path() -> Result { + let named_temp_file = Box::leak(Box::new(tempfile::Builder::new().suffix(".h").tempfile()?)); + Ok(named_temp_file + .path() + .to_str() + .ok_or_else(|| anyhow!(""))? + .to_string()) +} + +fn fallback_rust_output_path(rust_input_path: &str) -> Result { + Ok(Path::new(rust_input_path) + .parent() + .ok_or_else(|| anyhow!(""))? + .join("bridge_generated.rs") + .to_str() + .ok_or_else(|| anyhow!(""))? + .to_string()) +} + +fn fallback_dart_root(dart_output_path: &str) -> Result { + let mut res = canon_pathbuf(dart_output_path); + while res.pop() { + if res.join("pubspec.yaml").is_file() { + return res + .to_str() + .map(ToString::to_string) + .ok_or_else(|| anyhow!("Non-utf8 path")); + } + } + Err(anyhow!( + "Root of Dart library could not be inferred from Dart output" + )) +} + +fn fallback_class_name(rust_crate_dir: &str) -> Result { + let cargo_toml_path = Path::new(rust_crate_dir).join("Cargo.toml"); + let cargo_toml_content = fs::read_to_string(cargo_toml_path)?; + + let cargo_toml_value = cargo_toml_content.parse::()?; + let package_name = cargo_toml_value + .get("package") + .ok_or_else(|| anyhow!("no `package` in Cargo.toml"))? + .get("name") + .ok_or_else(|| anyhow!("no `name` in Cargo.toml"))? + .as_str() + .ok_or_else(|| anyhow!(""))?; + + Ok(package_name.to_case(Case::Pascal)) +} + +fn canon_path(sub_path: &str) -> String { + let path = canon_pathbuf(sub_path); + path_to_string(path).unwrap_or_else(|_| panic!("fail to parse path: {}", sub_path)) +} + +fn canon_pathbuf(sub_path: &str) -> PathBuf { + let mut path = + env::current_dir().unwrap_or_else(|_| panic!("fail to parse path: {}", sub_path)); + path.push(sub_path); + path +} + +fn path_to_string(path: PathBuf) -> Result { + path.into_os_string().into_string() +} + +impl Opts { + pub fn dart_api_class_name(&self) -> String { + self.class_name.clone() + } + + pub fn dart_api_impl_class_name(&self) -> String { + format!("{}Impl", self.class_name) + } + + pub fn dart_wire_class_name(&self) -> String { + format!("{}Wire", self.class_name) + } + + /// Returns None if the path terminates in "..", or not utf8. + pub fn dart_output_path_name(&self) -> Option<&str> { + let name = Path::new(&self.dart_output_path); + let root = name.file_name()?.to_str()?; + if let Some((name, _)) = root.rsplit_once('.') { + Some(name) + } else { + Some(root) + } + } + + pub fn dart_output_freezed_path(&self) -> Option { + Some( + Path::new(&self.dart_output_path) + .with_extension("freezed.dart") + .to_str()? + .to_owned(), + ) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/error.rs b/libs/flutter_rust_bridge_codegen/src/error.rs new file mode 100644 index 000000000..9a8607d37 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/error.rs @@ -0,0 +1,32 @@ +use thiserror::Error; + +pub type Result = std::result::Result<(), Error>; + +#[derive(Error, Debug)] +pub enum Error { + #[error("rustfmt failed: {0}")] + Rustfmt(String), + #[error("dart fmt failed: {0}")] + Dartfmt(String), + #[error( + "ffigen could not find LLVM. + Please supply --llvm-path to flutter_rust_bridge_codegen, e.g.: + + flutter_rust_bridge_codegen .. --llvm-path " + )] + FfigenLlvm, + #[error("{0} is not a command, or not executable.")] + MissingExe(String), + #[error("{0}")] + StringError(String), +} + +impl Error { + pub fn str(msg: &str) -> Self { + Self::StringError(msg.to_owned()) + } + + pub fn string(msg: String) -> Self { + Self::StringError(msg) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/c/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/c/mod.rs new file mode 100644 index 000000000..2a2410dbc --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/c/mod.rs @@ -0,0 +1,14 @@ +pub fn generate_dummy(func_names: &[String]) -> String { + format!( + r#"static int64_t dummy_method_to_enforce_bundling(void) {{ + int64_t dummy_var = 0; +{} + return dummy_var; +}}"#, + func_names + .iter() + .map(|func_name| { format!(" dummy_var ^= ((int64_t) (void*) {});", func_name) }) + .collect::>() + .join("\n"), + ) +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/mod.rs new file mode 100644 index 000000000..afe35527f --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/mod.rs @@ -0,0 +1,393 @@ +mod ty; +mod ty_boxed; +mod ty_delegate; +mod ty_enum; +mod ty_general_list; +mod ty_optional; +mod ty_primitive; +mod ty_primitive_list; +mod ty_struct; + +use std::collections::HashSet; + +pub use ty::*; +pub use ty_boxed::*; +pub use ty_delegate::*; +pub use ty_enum::*; +pub use ty_general_list::*; +pub use ty_optional::*; +pub use ty_primitive::*; +pub use ty_primitive_list::*; +pub use ty_struct::*; + +use convert_case::{Case, Casing}; +use log::debug; + +use crate::ir::IrType::*; +use crate::ir::*; +use crate::others::*; + +pub struct Output { + pub file_prelude: DartBasicCode, + pub decl_code: DartBasicCode, + pub impl_code: DartBasicCode, +} + +pub fn generate( + ir_file: &IrFile, + dart_api_class_name: &str, + dart_api_impl_class_name: &str, + dart_wire_class_name: &str, + dart_output_file_root: &str, +) -> (Output, bool) { + let distinct_types = ir_file.distinct_types(true, true); + let distinct_input_types = ir_file.distinct_types(true, false); + let distinct_output_types = ir_file.distinct_types(false, true); + debug!("distinct_input_types={:?}", distinct_input_types); + debug!("distinct_output_types={:?}", distinct_output_types); + + let dart_func_signatures_and_implementations = ir_file + .funcs + .iter() + .map(generate_api_func) + .collect::>(); + let dart_structs = distinct_types + .iter() + .map(|ty| TypeDartGenerator::new(ty.clone(), ir_file).structs()) + .collect::>(); + let dart_api2wire_funcs = distinct_input_types + .iter() + .map(|ty| generate_api2wire_func(ty, ir_file)) + .collect::>(); + let dart_api_fill_to_wire_funcs = distinct_input_types + .iter() + .map(|ty| generate_api_fill_to_wire_func(ty, ir_file)) + .collect::>(); + let dart_wire2api_funcs = distinct_output_types + .iter() + .map(|ty| generate_wire2api_func(ty, ir_file)) + .collect::>(); + + let needs_freezed = distinct_types.iter().any(|ty| match ty { + EnumRef(e) if e.is_struct => true, + StructRef(s) if s.freezed => true, + _ => false, + }); + let freezed_header = if needs_freezed { + DartBasicCode { + import: "import 'package:freezed_annotation/freezed_annotation.dart';".to_string(), + part: format!("part '{}.freezed.dart';", dart_output_file_root), + body: "".to_string(), + } + } else { + DartBasicCode::default() + }; + + let imports = ir_file + .struct_pool + .values() + .flat_map(|s| s.dart_metadata.iter().flat_map(|it| &it.library)) + .collect::>(); + + let import_header = if !imports.is_empty() { + DartBasicCode { + import: imports + .iter() + .map(|it| match &it.alias { + Some(alias) => format!("import '{}' as {};", it.uri, alias), + _ => format!("import '{}';", it.uri), + }) + .collect::>() + .join("\n"), + part: "".to_string(), + body: "".to_string(), + } + } else { + DartBasicCode::default() + }; + + let common_header = DartBasicCode { + import: "import 'dart:convert'; + import 'dart:typed_data';" + .to_string(), + part: "".to_string(), + body: "".to_string(), + }; + + let decl_body = format!( + "abstract class {} {{ + {} + }} + + {} + ", + dart_api_class_name, + dart_func_signatures_and_implementations + .iter() + .map(|(sig, _, comm)| format!("{}{}", comm, sig)) + .collect::>() + .join("\n\n"), + dart_structs.join("\n\n"), + ); + + let impl_body = format!( + "class {dart_api_impl_class_name} extends FlutterRustBridgeBase<{dart_wire_class_name}> implements {dart_api_class_name} {{ + factory {dart_api_impl_class_name}(ffi.DynamicLibrary dylib) => {dart_api_impl_class_name}.raw({dart_wire_class_name}(dylib)); + + {dart_api_impl_class_name}.raw({dart_wire_class_name} inner) : super(inner); + + {} + + // Section: api2wire + {} + + // Section: api_fill_to_wire + {} + }} + + // Section: wire2api + {} + ", + dart_func_signatures_and_implementations + .iter() + .map(|(_, imp, _)| imp.clone()) + .collect::>() + .join("\n\n"), + dart_api2wire_funcs.join("\n\n"), + dart_api_fill_to_wire_funcs.join("\n\n"), + dart_wire2api_funcs.join("\n\n"), + dart_api_impl_class_name = dart_api_impl_class_name, + dart_wire_class_name = dart_wire_class_name, + dart_api_class_name = dart_api_class_name, + ); + + let decl_code = &common_header + + &freezed_header + + &import_header + + &DartBasicCode { + import: "".to_string(), + part: "".to_string(), + body: decl_body, + }; + + let impl_code = &common_header + + &DartBasicCode { + import: "import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';".to_string(), + part: "".to_string(), + body: impl_body, + }; + + let file_prelude = DartBasicCode { + import: format!("{} + + // ignore_for_file: non_constant_identifier_names, unused_element, duplicate_ignore, directives_ordering, curly_braces_in_flow_control_structures, unnecessary_lambdas, slash_for_doc_comments, prefer_const_literals_to_create_immutables, implicit_dynamic_list_literal, duplicate_import, unused_import, prefer_single_quotes, prefer_const_constructors + ", + CODE_HEADER + ), + part: "".to_string(), + body: "".to_string(), + }; + + ( + Output { + file_prelude, + decl_code, + impl_code, + }, + needs_freezed, + ) +} + +fn generate_api_func(func: &IrFunc) -> (String, String, String) { + let raw_func_param_list = func + .inputs + .iter() + .map(|input| { + format!( + "{}{} {}", + input.ty.dart_required_modifier(), + input.ty.dart_api_type(), + input.name.dart_style() + ) + }) + .collect::>(); + + let full_func_param_list = [raw_func_param_list, vec!["dynamic hint".to_string()]].concat(); + + let wire_param_list = [ + if func.mode.has_port_argument() { + vec!["port_".to_string()] + } else { + vec![] + }, + func.inputs + .iter() + .map(|input| { + // edge case: ffigen performs its own bool-to-int conversions + if let IrType::Primitive(IrTypePrimitive::Bool) = input.ty { + input.name.dart_style() + } else { + format!( + "_api2wire_{}({})", + &input.ty.safe_ident(), + &input.name.dart_style() + ) + } + }) + .collect::>(), + ] + .concat(); + + let partial = format!( + "{} {}({{ {} }})", + func.mode.dart_return_type(&func.output.dart_api_type()), + func.name.to_case(Case::Camel), + full_func_param_list.join(","), + ); + + let execute_func_name = match func.mode { + IrFuncMode::Normal => "executeNormal", + IrFuncMode::Sync => "executeSync", + IrFuncMode::Stream => "executeStream", + }; + + let signature = format!("{};", partial); + + let comments = dart_comments(&func.comments); + + let task_common_args = format!( + " + constMeta: const FlutterRustBridgeTaskConstMeta( + debugName: \"{}\", + argNames: [{}], + ), + argValues: [{}], + hint: hint, + ", + func.name, + func.inputs + .iter() + .map(|input| format!("\"{}\"", input.name.dart_style())) + .collect::>() + .join(", "), + func.inputs + .iter() + .map(|input| input.name.dart_style()) + .collect::>() + .join(", "), + ); + + let implementation = match func.mode { + IrFuncMode::Sync => format!( + "{} => {}(FlutterRustBridgeSyncTask( + callFfi: () => inner.{}({}), + {} + ));", + partial, + execute_func_name, + func.wire_func_name(), + wire_param_list.join(", "), + task_common_args, + ), + _ => format!( + "{} => {}(FlutterRustBridgeTask( + callFfi: (port_) => inner.{}({}), + parseSuccessData: _wire2api_{}, + {} + ));", + partial, + execute_func_name, + func.wire_func_name(), + wire_param_list.join(", "), + func.output.safe_ident(), + task_common_args, + ), + }; + + (signature, implementation, comments) +} + +fn generate_api2wire_func(ty: &IrType, ir_file: &IrFile) -> String { + if let Some(body) = TypeDartGenerator::new(ty.clone(), ir_file).api2wire_body() { + format!( + "{} _api2wire_{}({} raw) {{ + {} + }} + ", + ty.dart_wire_type(), + ty.safe_ident(), + ty.dart_api_type(), + body, + ) + } else { + "".to_string() + } +} + +fn generate_api_fill_to_wire_func(ty: &IrType, ir_file: &IrFile) -> String { + if let Some(body) = TypeDartGenerator::new(ty.clone(), ir_file).api_fill_to_wire_body() { + let target_wire_type = match ty { + Optional(inner) => &inner.inner, + it => it, + }; + + format!( + "void _api_fill_to_wire_{}({} apiObj, {} wireObj) {{ + {} + }}", + ty.safe_ident(), + ty.dart_api_type(), + target_wire_type.dart_wire_type(), + body, + ) + } else { + "".to_string() + } +} + +fn generate_wire2api_func(ty: &IrType, ir_file: &IrFile) -> String { + let body = TypeDartGenerator::new(ty.clone(), ir_file).wire2api_body(); + + format!( + "{} _wire2api_{}(dynamic raw) {{ + {} + }} + ", + ty.dart_api_type(), + ty.safe_ident(), + body, + ) +} + +fn gen_wire2api_simple_type_cast(s: &str) -> String { + format!("return raw as {};", s) +} + +/// A trailing newline is included if comments is not empty. +fn dart_comments(comments: &[IrComment]) -> String { + let mut comments = comments + .iter() + .map(IrComment::comment) + .collect::>() + .join("\n"); + if !comments.is_empty() { + comments.push('\n'); + } + comments +} +fn dart_metadata(metadata: &[IrDartAnnotation]) -> String { + let mut metadata = metadata + .iter() + .map(|it| match &it.library { + Some(IrDartImport { + alias: Some(alias), .. + }) => format!("@{}.{}", alias, it.content), + _ => format!("@{}", it.content), + }) + .collect::>() + .join("\n"); + if !metadata.is_empty() { + metadata.push('\n'); + } + metadata +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty.rs new file mode 100644 index 000000000..dd8004ed9 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty.rs @@ -0,0 +1,64 @@ +use crate::generator::dart::*; +use enum_dispatch::enum_dispatch; + +#[enum_dispatch] +pub trait TypeDartGeneratorTrait { + fn api2wire_body(&self) -> Option; + + fn api_fill_to_wire_body(&self) -> Option { + None + } + + fn wire2api_body(&self) -> String { + "".to_string() + } + + fn structs(&self) -> String { + "".to_string() + } +} + +#[derive(Debug, Clone)] +pub struct TypeGeneratorContext<'a> { + pub ir_file: &'a IrFile, +} + +#[macro_export] +macro_rules! type_dart_generator_struct { + ($cls:ident, $ir_cls:ty) => { + #[derive(Debug, Clone)] + pub struct $cls<'a> { + pub ir: $ir_cls, + pub context: TypeGeneratorContext<'a>, + } + }; +} + +#[enum_dispatch(TypeDartGeneratorTrait)] +#[derive(Debug, Clone)] +pub enum TypeDartGenerator<'a> { + Primitive(TypePrimitiveGenerator<'a>), + Delegate(TypeDelegateGenerator<'a>), + PrimitiveList(TypePrimitiveListGenerator<'a>), + Optional(TypeOptionalGenerator<'a>), + GeneralList(TypeGeneralListGenerator<'a>), + StructRef(TypeStructRefGenerator<'a>), + Boxed(TypeBoxedGenerator<'a>), + EnumRef(TypeEnumRefGenerator<'a>), +} + +impl<'a> TypeDartGenerator<'a> { + pub fn new(ty: IrType, ir_file: &'a IrFile) -> Self { + let context = TypeGeneratorContext { ir_file }; + match ty { + Primitive(ir) => TypePrimitiveGenerator { ir, context }.into(), + Delegate(ir) => TypeDelegateGenerator { ir, context }.into(), + PrimitiveList(ir) => TypePrimitiveListGenerator { ir, context }.into(), + Optional(ir) => TypeOptionalGenerator { ir, context }.into(), + GeneralList(ir) => TypeGeneralListGenerator { ir, context }.into(), + StructRef(ir) => TypeStructRefGenerator { ir, context }.into(), + Boxed(ir) => TypeBoxedGenerator { ir, context }.into(), + EnumRef(ir) => TypeEnumRefGenerator { ir, context }.into(), + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_boxed.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_boxed.rs new file mode 100644 index 000000000..84c2b3675 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_boxed.rs @@ -0,0 +1,45 @@ +use crate::generator::dart::gen_wire2api_simple_type_cast; +use crate::generator::dart::ty::*; +use crate::ir::IrType::{EnumRef, Primitive, StructRef}; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeBoxedGenerator, IrTypeBoxed); + +impl TypeDartGeneratorTrait for TypeBoxedGenerator<'_> { + fn api2wire_body(&self) -> Option { + Some(match &*self.ir.inner { + Primitive(_) => { + format!("return inner.new_{}(raw);", self.ir.safe_ident()) + } + inner => { + format!( + "final ptr = inner.new_{}(); + _api_fill_to_wire_{}(raw, ptr.ref); + return ptr;", + self.ir.safe_ident(), + inner.safe_ident(), + ) + } + }) + } + + fn api_fill_to_wire_body(&self) -> Option { + if !matches!(*self.ir.inner, Primitive(_)) { + Some(format!( + " _api_fill_to_wire_{}(apiObj, wireObj.ref);", + self.ir.inner.safe_ident() + )) + } else { + None + } + } + + fn wire2api_body(&self) -> String { + match &*self.ir.inner { + StructRef(inner) => format!("return _wire2api_{}(raw);", inner.safe_ident()), + EnumRef(inner) => format!("return _wire2api_{}(raw);", inner.safe_ident()), + _ => gen_wire2api_simple_type_cast(&self.ir.dart_api_type()), + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_delegate.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_delegate.rs new file mode 100644 index 000000000..b585ff3f7 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_delegate.rs @@ -0,0 +1,42 @@ +use crate::generator::dart::gen_wire2api_simple_type_cast; +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeDelegateGenerator, IrTypeDelegate); + +impl TypeDartGeneratorTrait for TypeDelegateGenerator<'_> { + fn api2wire_body(&self) -> Option { + Some(match self.ir { + IrTypeDelegate::String => { + "return _api2wire_uint_8_list(utf8.encoder.convert(raw));".to_string() + } + IrTypeDelegate::SyncReturnVecU8 => "/*unsupported*/".to_string(), + IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + format!( + "return _api2wire_{}(raw);", + self.ir.get_delegate().safe_ident() + ) + } + IrTypeDelegate::StringList => "final ans = inner.new_StringList(raw.length); + for (var i = 0; i < raw.length; i++) { + ans.ref.ptr[i] = _api2wire_String(raw[i]); + } + return ans;" + .to_owned(), + }) + } + + fn wire2api_body(&self) -> String { + match &self.ir { + IrTypeDelegate::String + | IrTypeDelegate::SyncReturnVecU8 + | IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + gen_wire2api_simple_type_cast(&self.ir.dart_api_type()) + } + IrTypeDelegate::StringList => { + "return (raw as List).cast();".to_owned() + } + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_enum.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_enum.rs new file mode 100644 index 000000000..fc361b4c8 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_enum.rs @@ -0,0 +1,207 @@ +use crate::generator::dart::dart_comments; +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeEnumRefGenerator, IrTypeEnumRef); + +impl TypeDartGeneratorTrait for TypeEnumRefGenerator<'_> { + fn api2wire_body(&self) -> Option { + if !self.ir.is_struct { + Some("return raw.index;".to_owned()) + } else { + None + } + } + + fn api_fill_to_wire_body(&self) -> Option { + if self.ir.is_struct { + Some( + self.ir + .get(self.context.ir_file) + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| { + if let IrVariantKind::Value = &variant.kind { + format!( + "if (apiObj is {}) {{ wireObj.tag = {}; return; }}", + variant.name, idx + ) + } else { + let r = format!("wireObj.kind.ref.{}.ref", variant.name); + let body: Vec<_> = match &variant.kind { + IrVariantKind::Struct(st) => st + .fields + .iter() + .map(|field| { + format!( + "{}.{} = _api2wire_{}(apiObj.{});", + r, + field.name.rust_style(), + field.ty.safe_ident(), + field.name.dart_style() + ) + }) + .collect(), + _ => unreachable!(), + }; + format!( + "if (apiObj is {0}) {{ + wireObj.tag = {1}; + wireObj.kind = inner.inflate_{2}_{0}(); + {3} + }}", + variant.name, + idx, + self.ir.name, + body.join("\n") + ) + } + }) + .collect::>() + .join("\n"), + ) + } else { + None + } + } + + fn wire2api_body(&self) -> String { + if self.ir.is_struct { + let enu = self.ir.get(self.context.ir_file); + let variants = enu + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| { + let args = match &variant.kind { + IrVariantKind::Value => "".to_owned(), + IrVariantKind::Struct(st) => st + .fields + .iter() + .enumerate() + .map(|(idx, field)| { + let val = format!( + "_wire2api_{}(raw[{}]),", + field.ty.safe_ident(), + idx + 1 + ); + if st.is_fields_named { + format!("{}: {}", field.name.dart_style(), val) + } else { + val + } + }) + .collect::>() + .join(""), + }; + format!("case {}: return {}({});", idx, variant.name, args) + }) + .collect::>(); + format!( + "switch (raw[0]) {{ + {} + default: throw Exception(\"unreachable\"); + }}", + variants.join("\n"), + ) + } else { + format!("return {}.values[raw];", self.ir.name) + } + } + + fn structs(&self) -> String { + let src = self.ir.get(self.context.ir_file); + + let comments = dart_comments(&src.comments); + if src.is_struct() { + let variants = src + .variants() + .iter() + .map(|variant| { + let args = match &variant.kind { + IrVariantKind::Value => "".to_owned(), + IrVariantKind::Struct(IrStruct { + is_fields_named: false, + fields, + .. + }) => { + let types = fields.iter().map(|field| &field.ty).collect::>(); + let split = optional_boundary_index(&types); + let types = fields + .iter() + .map(|field| { + format!( + "{}{} {},", + dart_comments(&field.comments), + field.ty.dart_api_type(), + field.name.dart_style() + ) + }) + .collect::>(); + if let Some(idx) = split { + let before = &types[..idx]; + let after = &types[idx..]; + format!("{}[{}]", before.join(""), after.join("")) + } else { + types.join("") + } + } + IrVariantKind::Struct(st) => { + let fields = st + .fields + .iter() + .map(|field| { + format!( + "{}{}{} {},", + dart_comments(&field.comments), + field.ty.dart_required_modifier(), + field.ty.dart_api_type(), + field.name.dart_style() + ) + }) + .collect::>(); + format!("{{ {} }}", fields.join("")) + } + }; + format!( + "{}const factory {}.{}({}) = {};", + dart_comments(&variant.comments), + self.ir.name, + variant.name.dart_style(), + args, + variant.name.rust_style(), + ) + }) + .collect::>(); + format!( + "@freezed + class {0} with _${0} {{ + {1} + }}", + self.ir.name, + variants.join("\n") + ) + } else { + let variants = src + .variants() + .iter() + .map(|variant| { + format!( + "{}{},", + dart_comments(&variant.comments), + variant.name.rust_style() + ) + }) + .collect::>() + .join("\n"); + format!( + "{}enum {} {{ + {} + }}", + comments, self.ir.name, variants + ) + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_general_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_general_list.rs new file mode 100644 index 000000000..000f7288f --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_general_list.rs @@ -0,0 +1,27 @@ +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeGeneralListGenerator, IrTypeGeneralList); + +impl TypeDartGeneratorTrait for TypeGeneralListGenerator<'_> { + fn api2wire_body(&self) -> Option { + // NOTE the memory strategy is same as PrimitiveList, see comments there. + Some(format!( + "final ans = inner.new_{}(raw.length); + for (var i = 0; i < raw.length; ++i) {{ + _api_fill_to_wire_{}(raw[i], ans.ref.ptr[i]); + }} + return ans;", + self.ir.safe_ident(), + self.ir.inner.safe_ident() + )) + } + + fn wire2api_body(&self) -> String { + format!( + "return (raw as List).map(_wire2api_{}).toList();", + self.ir.inner.safe_ident() + ) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_optional.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_optional.rs new file mode 100644 index 000000000..5b7e60d27 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_optional.rs @@ -0,0 +1,31 @@ +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeOptionalGenerator, IrTypeOptional); + +impl TypeDartGeneratorTrait for TypeOptionalGenerator<'_> { + fn api2wire_body(&self) -> Option { + Some(format!( + "return raw == null ? ffi.nullptr : _api2wire_{}(raw);", + self.ir.inner.safe_ident() + )) + } + + fn api_fill_to_wire_body(&self) -> Option { + if !self.ir.needs_initialization() || self.ir.is_list() { + return None; + } + Some(format!( + "if (apiObj != null) _api_fill_to_wire_{}(apiObj, wireObj);", + self.ir.inner.safe_ident() + )) + } + + fn wire2api_body(&self) -> String { + format!( + "return raw == null ? null : _wire2api_{}(raw);", + self.ir.inner.safe_ident() + ) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive.rs new file mode 100644 index 000000000..0ed9aa686 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive.rs @@ -0,0 +1,22 @@ +use crate::generator::dart::gen_wire2api_simple_type_cast; +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypePrimitiveGenerator, IrTypePrimitive); + +impl TypeDartGeneratorTrait for TypePrimitiveGenerator<'_> { + fn api2wire_body(&self) -> Option { + Some(match self.ir { + IrTypePrimitive::Bool => "return raw ? 1 : 0;".to_owned(), + _ => "return raw;".to_string(), + }) + } + + fn wire2api_body(&self) -> String { + match self.ir { + IrTypePrimitive::Unit => "return;".to_owned(), + _ => gen_wire2api_simple_type_cast(&self.ir.dart_api_type()), + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive_list.rs new file mode 100644 index 000000000..d07c24d6b --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_primitive_list.rs @@ -0,0 +1,30 @@ +use crate::generator::dart::gen_wire2api_simple_type_cast; +use crate::generator::dart::ty::*; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypePrimitiveListGenerator, IrTypePrimitiveList); + +impl TypeDartGeneratorTrait for TypePrimitiveListGenerator<'_> { + fn api2wire_body(&self) -> Option { + // NOTE Dart code *only* allocates memory. It never *release* memory by itself. + // Instead, Rust receives that pointer and now it is in control of Rust. + // Therefore, *never* continue to use this pointer after you have passed the pointer + // to Rust. + // NOTE WARN: Never use the [calloc] provided by Dart FFI to allocate any memory. + // Instead, ask Rust to allocate some memory and return raw pointers. Otherwise, + // memory will be allocated in one dylib (e.g. libflutter.so), and then be released + // by another dylib (e.g. my_rust_code.so), especially in Android platform. It can be + // undefined behavior. + Some(format!( + "final ans = inner.new_{}(raw.length); + ans.ref.ptr.asTypedList(raw.length).setAll(0, raw); + return ans;", + self.ir.safe_ident(), + )) + } + + fn wire2api_body(&self) -> String { + gen_wire2api_simple_type_cast(&self.ir.dart_api_type()) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_struct.rs b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_struct.rs new file mode 100644 index 000000000..fa67bd32a --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/dart/ty_struct.rs @@ -0,0 +1,135 @@ +use crate::generator::dart::ty::*; +use crate::generator::dart::{dart_comments, dart_metadata}; +use crate::ir::*; +use crate::type_dart_generator_struct; + +type_dart_generator_struct!(TypeStructRefGenerator, IrTypeStructRef); + +impl TypeDartGeneratorTrait for TypeStructRefGenerator<'_> { + fn api2wire_body(&self) -> Option { + None + } + + fn api_fill_to_wire_body(&self) -> Option { + let s = self.ir.get(self.context.ir_file); + Some( + s.fields + .iter() + .map(|field| { + format!( + "wireObj.{} = _api2wire_{}(apiObj.{});", + field.name.rust_style(), + field.ty.safe_ident(), + field.name.dart_style() + ) + }) + .collect::>() + .join("\n"), + ) + } + + fn wire2api_body(&self) -> String { + let s = self.ir.get(self.context.ir_file); + let inner = s + .fields + .iter() + .enumerate() + .map(|(idx, field)| { + format!( + "{}: _wire2api_{}(arr[{}]),", + field.name.dart_style(), + field.ty.safe_ident(), + idx + ) + }) + .collect::>() + .join("\n"); + + format!( + "final arr = raw as List; + if (arr.length != {}) throw Exception('unexpected arr length: expect {} but see ${{arr.length}}'); + return {}({});", + s.fields.len(), + s.fields.len(), + s.name, inner, + ) + } + + fn structs(&self) -> String { + let src = self.ir.get(self.context.ir_file); + let comments = dart_comments(&src.comments); + let metadata = dart_metadata(&src.dart_metadata); + + if src.using_freezed() { + let constructor_params = src + .fields + .iter() + .map(|f| { + format!( + "{} {} {},", + f.ty.dart_required_modifier(), + f.ty.dart_api_type(), + f.name.dart_style() + ) + }) + .collect::>() + .join(""); + + format!( + "{}{}class {} with _${} {{ + const factory {}({{{}}}) = _{}; + }}", + comments, + metadata, + self.ir.name, + self.ir.name, + self.ir.name, + constructor_params, + self.ir.name + ) + } else { + let field_declarations = src + .fields + .iter() + .map(|f| { + let comments = dart_comments(&f.comments); + format!( + "{}{} {} {};", + comments, + if f.is_final { "final" } else { "" }, + f.ty.dart_api_type(), + f.name.dart_style() + ) + }) + .collect::>() + .join("\n"); + + let constructor_params = src + .fields + .iter() + .map(|f| { + format!( + "{}this.{},", + f.ty.dart_required_modifier(), + f.name.dart_style() + ) + }) + .collect::>() + .join(""); + + format!( + "{}{}class {} {{ + {} + + {}({{{}}}); + }}", + comments, + metadata, + self.ir.name, + field_declarations, + self.ir.name, + constructor_params + ) + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/mod.rs new file mode 100644 index 000000000..3891c02e3 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/mod.rs @@ -0,0 +1,3 @@ +pub mod c; +pub mod dart; +pub mod rust; diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/mod.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/mod.rs new file mode 100644 index 000000000..0b0d1df88 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/mod.rs @@ -0,0 +1,481 @@ +mod ty; +mod ty_boxed; +mod ty_delegate; +mod ty_enum; +mod ty_general_list; +mod ty_optional; +mod ty_primitive; +mod ty_primitive_list; +mod ty_struct; + +pub use ty::*; +pub use ty_boxed::*; +pub use ty_delegate::*; +pub use ty_enum::*; +pub use ty_general_list::*; +pub use ty_optional::*; +pub use ty_primitive::*; +pub use ty_primitive_list::*; +pub use ty_struct::*; + +use std::collections::HashSet; + +use crate::ir::IrType::*; +use crate::ir::*; +use crate::others::*; + +pub const HANDLER_NAME: &str = "FLUTTER_RUST_BRIDGE_HANDLER"; + +pub struct Output { + pub code: String, + pub extern_func_names: Vec, +} + +pub fn generate(ir_file: &IrFile, rust_wire_mod: &str) -> Output { + let mut generator = Generator::new(); + let code = generator.generate(ir_file, rust_wire_mod); + + Output { + code, + extern_func_names: generator.extern_func_collector.names, + } +} + +struct Generator { + extern_func_collector: ExternFuncCollector, +} + +impl Generator { + fn new() -> Self { + Self { + extern_func_collector: ExternFuncCollector::new(), + } + } + + fn generate(&mut self, ir_file: &IrFile, rust_wire_mod: &str) -> String { + let mut lines: Vec = vec![]; + + let distinct_input_types = ir_file.distinct_types(true, false); + let distinct_output_types = ir_file.distinct_types(false, true); + + lines.push(r#"#![allow(non_camel_case_types, unused, clippy::redundant_closure, clippy::useless_conversion, clippy::unit_arg, clippy::double_parens, non_snake_case)]"#.to_string()); + lines.push(CODE_HEADER.to_string()); + + lines.push(String::new()); + lines.push(format!("use crate::{}::*;", rust_wire_mod)); + lines.push("use flutter_rust_bridge::*;".to_string()); + lines.push(String::new()); + + lines.push(self.section_header_comment("imports")); + lines.extend(self.generate_imports( + ir_file, + rust_wire_mod, + &distinct_input_types, + &distinct_output_types, + )); + lines.push(String::new()); + + lines.push(self.section_header_comment("wire functions")); + lines.extend( + ir_file + .funcs + .iter() + .map(|f| self.generate_wire_func(f, ir_file)), + ); + + lines.push(self.section_header_comment("wire structs")); + lines.extend( + distinct_input_types + .iter() + .map(|ty| self.generate_wire_struct(ty, ir_file)), + ); + lines.extend( + distinct_input_types + .iter() + .map(|ty| TypeRustGenerator::new(ty.clone(), ir_file).structs()), + ); + + lines.push(self.section_header_comment("wrapper structs")); + lines.extend( + distinct_output_types + .iter() + .filter_map(|ty| self.generate_wrapper_struct(ty, ir_file)), + ); + lines.push(self.section_header_comment("static checks")); + let static_checks: Vec<_> = distinct_output_types + .iter() + .filter_map(|ty| self.generate_static_checks(ty, ir_file)) + .collect(); + if !static_checks.is_empty() { + lines.push("const _: fn() = || {".to_owned()); + lines.extend(static_checks); + lines.push("};".to_owned()); + } + + lines.push(self.section_header_comment("allocate functions")); + lines.extend( + distinct_input_types + .iter() + .map(|f| self.generate_allocate_funcs(f, ir_file)), + ); + + lines.push(self.section_header_comment("impl Wire2Api")); + lines.push(self.generate_wire2api_misc().to_string()); + lines.extend( + distinct_input_types + .iter() + .map(|ty| self.generate_wire2api_func(ty, ir_file)), + ); + + lines.push(self.section_header_comment("impl NewWithNullPtr")); + lines.push(self.generate_new_with_nullptr_misc().to_string()); + lines.extend( + distinct_input_types + .iter() + .map(|ty| self.generate_new_with_nullptr_func(ty, ir_file)), + ); + + lines.push(self.section_header_comment("impl IntoDart")); + lines.extend( + distinct_output_types + .iter() + .map(|ty| self.generate_impl_intodart(ty, ir_file)), + ); + + lines.push(self.section_header_comment("executor")); + lines.push(self.generate_executor(ir_file)); + + lines.push(self.section_header_comment("sync execution mode utility")); + lines.push(self.generate_sync_execution_mode_utility()); + + lines.join("\n") + } + + fn section_header_comment(&self, section_name: &str) -> String { + format!("// Section: {}\n", section_name) + } + + fn generate_imports( + &self, + ir_file: &IrFile, + rust_wire_mod: &str, + distinct_input_types: &[IrType], + distinct_output_types: &[IrType], + ) -> impl Iterator { + let input_type_imports = distinct_input_types + .iter() + .map(|api_type| generate_import(api_type, ir_file)); + let output_type_imports = distinct_output_types + .iter() + .map(|api_type| generate_import(api_type, ir_file)); + + input_type_imports + .chain(output_type_imports) + // Filter out `None` and unwrap + .flatten() + // Don't include imports from the API file + .filter(|import| !import.starts_with(&format!("use crate::{}::", rust_wire_mod))) + // de-duplicate + .collect::>() + .into_iter() + } + + fn generate_executor(&mut self, ir_file: &IrFile) -> String { + if ir_file.has_executor { + "/* nothing since executor detected */".to_string() + } else { + format!( + "support::lazy_static! {{ + pub static ref {}: support::DefaultHandler = Default::default(); + }} + ", + HANDLER_NAME + ) + } + } + + fn generate_sync_execution_mode_utility(&mut self) -> String { + self.extern_func_collector.generate( + "free_WireSyncReturnStruct", + &["val: support::WireSyncReturnStruct"], + None, + "unsafe { let _ = support::vec_from_leak_ptr(val.ptr, val.len); }", + ) + } + + fn generate_wire_func(&mut self, func: &IrFunc, ir_file: &IrFile) -> String { + let params = [ + if func.mode.has_port_argument() { + vec!["port_: i64".to_string()] + } else { + vec![] + }, + func.inputs + .iter() + .map(|field| { + format!( + "{}: {}{}", + field.name.rust_style(), + field.ty.rust_wire_modifier(), + field.ty.rust_wire_type() + ) + }) + .collect::>(), + ] + .concat(); + + let inner_func_params = [ + match func.mode { + IrFuncMode::Normal | IrFuncMode::Sync => vec![], + IrFuncMode::Stream => vec!["task_callback.stream_sink()".to_string()], + }, + func.inputs + .iter() + .map(|field| format!("api_{}", field.name.rust_style())) + .collect::>(), + ] + .concat(); + + let wrap_info_obj = format!( + "WrapInfo{{ debug_name: \"{}\", port: {}, mode: FfiCallMode::{} }}", + func.name, + if func.mode.has_port_argument() { + "Some(port_)" + } else { + "None" + }, + func.mode.ffi_call_mode(), + ); + + let code_wire2api = func + .inputs + .iter() + .map(|field| { + format!( + "let api_{} = {}.wire2api();", + field.name.rust_style(), + field.name.rust_style() + ) + }) + .collect::>() + .join(""); + + let code_call_inner_func = TypeRustGenerator::new(func.output.clone(), ir_file) + .wrap_obj(format!("{}({})", func.name, inner_func_params.join(", "))); + let code_call_inner_func_result = if func.fallible { + code_call_inner_func + } else { + format!("Ok({})", code_call_inner_func) + }; + + let (handler_func_name, return_type, code_closure) = match func.mode { + IrFuncMode::Sync => ( + "wrap_sync", + Some("support::WireSyncReturnStruct"), + format!( + "{} + {}", + code_wire2api, code_call_inner_func_result, + ), + ), + IrFuncMode::Normal | IrFuncMode::Stream => ( + "wrap", + None, + format!( + "{} + move |task_callback| {} + ", + code_wire2api, code_call_inner_func_result, + ), + ), + }; + + self.extern_func_collector.generate( + &func.wire_func_name(), + ¶ms + .iter() + .map(std::ops::Deref::deref) + .collect::>(), + return_type, + &format!( + " + {}.{}({}, move || {{ + {} + }}) + ", + HANDLER_NAME, handler_func_name, wrap_info_obj, code_closure, + ), + ) + } + + fn generate_wire_struct(&mut self, ty: &IrType, ir_file: &IrFile) -> String { + // println!("generate_wire_struct: {:?}", ty); + if let Some(fields) = TypeRustGenerator::new(ty.clone(), ir_file).wire_struct_fields() { + format!( + r###" + #[repr(C)] + #[derive(Clone)] + pub struct {} {{ + {} + }} + "###, + ty.rust_wire_type(), + fields.join(",\n"), + ) + } else { + "".to_string() + } + } + + fn generate_allocate_funcs(&mut self, ty: &IrType, ir_file: &IrFile) -> String { + // println!("generate_allocate_funcs: {:?}", ty); + TypeRustGenerator::new(ty.clone(), ir_file).allocate_funcs(&mut self.extern_func_collector) + } + + fn generate_wire2api_misc(&self) -> &'static str { + r"pub trait Wire2Api { + fn wire2api(self) -> T; + } + + impl Wire2Api> for *mut S + where + *mut S: Wire2Api + { + fn wire2api(self) -> Option { + if self.is_null() { + None + } else { + Some(self.wire2api()) + } + } + } + " + } + + fn generate_wire2api_func(&mut self, ty: &IrType, ir_file: &IrFile) -> String { + // println!("generate_wire2api_func: {:?}", ty); + if let Some(body) = TypeRustGenerator::new(ty.clone(), ir_file).wire2api_body() { + format!( + "impl Wire2Api<{}> for {} {{ + fn wire2api(self) -> {} {{ + {} + }} + }} + ", + ty.rust_api_type(), + ty.rust_wire_modifier() + &ty.rust_wire_type(), + ty.rust_api_type(), + body, + ) + } else { + "".to_string() + } + } + + fn generate_static_checks(&mut self, ty: &IrType, ir_file: &IrFile) -> Option { + TypeRustGenerator::new(ty.clone(), ir_file).static_checks() + } + + fn generate_wrapper_struct(&mut self, ty: &IrType, ir_file: &IrFile) -> Option { + match ty { + IrType::StructRef(_) | IrType::EnumRef(_) => { + TypeRustGenerator::new(ty.clone(), ir_file) + .wrapper_struct() + .map(|wrapper| { + format!( + r###" + #[derive(Clone)] + struct {}({}); + "###, + wrapper, + ty.rust_api_type(), + ) + }) + } + _ => None, + } + } + + fn generate_new_with_nullptr_misc(&self) -> &'static str { + "pub trait NewWithNullPtr { + fn new_with_null_ptr() -> Self; + } + + impl NewWithNullPtr for *mut T { + fn new_with_null_ptr() -> Self { + std::ptr::null_mut() + } + } + " + } + + fn generate_new_with_nullptr_func(&mut self, ty: &IrType, ir_file: &IrFile) -> String { + TypeRustGenerator::new(ty.clone(), ir_file) + .new_with_nullptr(&mut self.extern_func_collector) + } + + fn generate_impl_intodart(&mut self, ty: &IrType, ir_file: &IrFile) -> String { + // println!("generate_impl_intodart: {:?}", ty); + TypeRustGenerator::new(ty.clone(), ir_file).impl_intodart() + } +} + +pub fn generate_import(api_type: &IrType, ir_file: &IrFile) -> Option { + TypeRustGenerator::new(api_type.clone(), ir_file).imports() +} + +pub fn generate_list_allocate_func( + collector: &mut ExternFuncCollector, + safe_ident: &str, + list: &impl IrTypeTrait, + inner: &IrType, +) -> String { + collector.generate( + &format!("new_{}", safe_ident), + &["len: i32"], + Some(&[ + list.rust_wire_modifier().as_str(), + list.rust_wire_type().as_str() + ].concat()), + &format!( + "let wrap = {} {{ ptr: support::new_leak_vec_ptr(<{}{}>::new_with_null_ptr(), len), len }}; + support::new_leak_box_ptr(wrap)", + list.rust_wire_type(), + inner.rust_ptr_modifier(), + inner.rust_wire_type() + ), + ) +} + +pub struct ExternFuncCollector { + names: Vec, +} + +impl ExternFuncCollector { + fn new() -> Self { + ExternFuncCollector { names: vec![] } + } + + fn generate( + &mut self, + func_name: &str, + params: &[&str], + return_type: Option<&str>, + body: &str, + ) -> String { + self.names.push(func_name.to_string()); + + format!( + r#" + #[no_mangle] + pub extern "C" fn {}({}) {} {{ + {} + }} + "#, + func_name, + params.join(", "), + return_type.map_or("".to_string(), |r| format!("-> {}", r)), + body, + ) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty.rs new file mode 100644 index 000000000..827d6b8f1 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty.rs @@ -0,0 +1,96 @@ +use crate::generator::rust::*; +use enum_dispatch::enum_dispatch; + +#[enum_dispatch] +pub trait TypeRustGeneratorTrait { + fn wire2api_body(&self) -> Option; + + fn wire_struct_fields(&self) -> Option> { + None + } + + fn static_checks(&self) -> Option { + None + } + + fn wrapper_struct(&self) -> Option { + None + } + + fn self_access(&self, obj: String) -> String { + obj + } + + fn wrap_obj(&self, obj: String) -> String { + obj + } + + fn convert_to_dart(&self, obj: String) -> String { + format!("{}.into_dart()", obj) + } + + fn structs(&self) -> String { + "".to_string() + } + + fn allocate_funcs(&self, _collector: &mut ExternFuncCollector) -> String { + "".to_string() + } + + fn impl_intodart(&self) -> String { + "".to_string() + } + + fn new_with_nullptr(&self, _collector: &mut ExternFuncCollector) -> String { + "".to_string() + } + + fn imports(&self) -> Option { + None + } +} + +#[derive(Debug, Clone)] +pub struct TypeGeneratorContext<'a> { + pub ir_file: &'a IrFile, +} + +#[macro_export] +macro_rules! type_rust_generator_struct { + ($cls:ident, $ir_cls:ty) => { + #[derive(Debug, Clone)] + pub struct $cls<'a> { + pub ir: $ir_cls, + pub context: TypeGeneratorContext<'a>, + } + }; +} + +#[enum_dispatch(TypeRustGeneratorTrait)] +#[derive(Debug, Clone)] +pub enum TypeRustGenerator<'a> { + Primitive(TypePrimitiveGenerator<'a>), + Delegate(TypeDelegateGenerator<'a>), + PrimitiveList(TypePrimitiveListGenerator<'a>), + Optional(TypeOptionalGenerator<'a>), + GeneralList(TypeGeneralListGenerator<'a>), + StructRef(TypeStructRefGenerator<'a>), + Boxed(TypeBoxedGenerator<'a>), + EnumRef(TypeEnumRefGenerator<'a>), +} + +impl<'a> TypeRustGenerator<'a> { + pub fn new(ty: IrType, ir_file: &'a IrFile) -> Self { + let context = TypeGeneratorContext { ir_file }; + match ty { + Primitive(ir) => TypePrimitiveGenerator { ir, context }.into(), + Delegate(ir) => TypeDelegateGenerator { ir, context }.into(), + PrimitiveList(ir) => TypePrimitiveListGenerator { ir, context }.into(), + Optional(ir) => TypeOptionalGenerator { ir, context }.into(), + GeneralList(ir) => TypeGeneralListGenerator { ir, context }.into(), + StructRef(ir) => TypeStructRefGenerator { ir, context }.into(), + Boxed(ir) => TypeBoxedGenerator { ir, context }.into(), + EnumRef(ir) => TypeEnumRefGenerator { ir, context }.into(), + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_boxed.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_boxed.rs new file mode 100644 index 000000000..ab6d25d02 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_boxed.rs @@ -0,0 +1,62 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::{generate_import, ExternFuncCollector}; +use crate::ir::IrType::Primitive; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeBoxedGenerator, IrTypeBoxed); + +impl TypeRustGeneratorTrait for TypeBoxedGenerator<'_> { + fn wire2api_body(&self) -> Option { + let IrTypeBoxed { + inner: box_inner, + exist_in_real_api, + } = &self.ir; + Some(match (box_inner.as_ref(), exist_in_real_api) { + (IrType::Primitive(_), false) => "unsafe { *support::box_from_leak_ptr(self) }".into(), + (IrType::Primitive(_), true) => "unsafe { support::box_from_leak_ptr(self) }".into(), + _ => { + "let wrap = unsafe { support::box_from_leak_ptr(self) }; (*wrap).wire2api().into()" + .into() + } + }) + } + + fn wrapper_struct(&self) -> Option { + let src = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); + src.wrapper_struct() + } + + fn self_access(&self, obj: String) -> String { + format!("(*{})", obj) + } + + fn wrap_obj(&self, obj: String) -> String { + let src = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); + src.wrap_obj(self.self_access(obj)) + } + + fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { + match &*self.ir.inner { + Primitive(prim) => collector.generate( + &format!("new_{}", self.ir.safe_ident()), + &[&format!("value: {}", prim.rust_wire_type())], + Some(&format!("*mut {}", prim.rust_wire_type())), + "support::new_leak_box_ptr(value)", + ), + inner => collector.generate( + &format!("new_{}", self.ir.safe_ident()), + &[], + Some(&[self.ir.rust_wire_modifier(), self.ir.rust_wire_type()].concat()), + &format!( + "support::new_leak_box_ptr({}::new_with_null_ptr())", + inner.rust_wire_type() + ), + ), + } + } + + fn imports(&self) -> Option { + generate_import(&self.ir.inner, self.context.ir_file) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_delegate.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_delegate.rs new file mode 100644 index 000000000..9b67ba7dd --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_delegate.rs @@ -0,0 +1,45 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::{ + generate_list_allocate_func, ExternFuncCollector, TypeGeneralListGenerator, +}; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeDelegateGenerator, IrTypeDelegate); + +impl TypeRustGeneratorTrait for TypeDelegateGenerator<'_> { + fn wire2api_body(&self) -> Option { + Some(match &self.ir { + IrTypeDelegate::String => "let vec: Vec = self.wire2api(); + String::from_utf8_lossy(&vec).into_owned()" + .into(), + IrTypeDelegate::SyncReturnVecU8 => "/*unsupported*/".into(), + IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + "ZeroCopyBuffer(self.wire2api())".into() + } + IrTypeDelegate::StringList => TypeGeneralListGenerator::WIRE2API_BODY.to_string(), + }) + } + + fn wire_struct_fields(&self) -> Option> { + match &self.ir { + ty @ IrTypeDelegate::StringList => Some(vec![ + format!("ptr: *mut *mut {}", ty.get_delegate().rust_wire_type()), + "len: i32".to_owned(), + ]), + _ => None, + } + } + + fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { + match &self.ir { + list @ IrTypeDelegate::StringList => generate_list_allocate_func( + collector, + &self.ir.safe_ident(), + list, + &list.get_delegate(), + ), + _ => "".to_string(), + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_enum.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_enum.rs new file mode 100644 index 000000000..a0fc42ddb --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_enum.rs @@ -0,0 +1,343 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::ExternFuncCollector; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeEnumRefGenerator, IrTypeEnumRef); + +impl TypeRustGeneratorTrait for TypeEnumRefGenerator<'_> { + fn wire2api_body(&self) -> Option { + let enu = self.ir.get(self.context.ir_file); + Some(if self.ir.is_struct { + let variants = enu + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| match &variant.kind { + IrVariantKind::Value => { + format!("{} => {}::{},", idx, enu.name, variant.name) + } + IrVariantKind::Struct(st) => { + let fields: Vec<_> = st + .fields + .iter() + .map(|field| { + if st.is_fields_named { + format!("{0}: ans.{0}.wire2api()", field.name.rust_style()) + } else { + format!("ans.{}.wire2api()", field.name.rust_style()) + } + }) + .collect(); + let (left, right) = st.brackets_pair(); + format!( + "{} => unsafe {{ + let ans = support::box_from_leak_ptr(self.kind); + let ans = support::box_from_leak_ptr(ans.{2}); + {}::{2}{3}{4}{5} + }}", + idx, + enu.name, + variant.name, + left, + fields.join(","), + right + ) + } + }) + .collect::>(); + format!( + "match self.tag {{ + {} + _ => unreachable!(), + }}", + variants.join("\n"), + ) + } else { + let variants = enu + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| format!("{} => {}::{},", idx, enu.name, variant.name)) + .collect::>() + .join("\n"); + format!( + "match self {{ + {} + _ => unreachable!(\"Invalid variant for {}: {{}}\", self), + }}", + variants, enu.name + ) + }) + } + + fn structs(&self) -> String { + let src = self.ir.get(self.context.ir_file); + if !src.is_struct() { + return "".to_owned(); + } + let variant_structs = src + .variants() + .iter() + .map(|variant| { + let fields = match &variant.kind { + IrVariantKind::Value => vec![], + IrVariantKind::Struct(s) => s + .fields + .iter() + .map(|field| { + format!( + "{}: {}{},", + field.name.rust_style(), + field.ty.rust_wire_modifier(), + field.ty.rust_wire_type() + ) + }) + .collect(), + }; + format!( + "#[repr(C)] + #[derive(Clone)] + pub struct {}_{} {{ {} }}", + self.ir.name, + variant.name, + fields.join("\n") + ) + }) + .collect::>(); + let union_fields = src + .variants() + .iter() + .map(|variant| format!("{0}: *mut {1}_{0},", variant.name, self.ir.name)) + .collect::>(); + format!( + "#[repr(C)] + #[derive(Clone)] + pub struct {0} {{ tag: i32, kind: *mut {1}Kind }} + + #[repr(C)] + pub union {1}Kind {{ + {2} + }} + + {3}", + self.ir.rust_wire_type(), + self.ir.name, + union_fields.join("\n"), + variant_structs.join("\n\n") + ) + } + + fn static_checks(&self) -> Option { + let src = self.ir.get(self.context.ir_file); + src.wrapper_name.as_ref()?; + + let branches: Vec<_> = src + .variants() + .iter() + .map(|variant| match &variant.kind { + IrVariantKind::Value => format!("{}::{} => {{}}", src.name, variant.name), + IrVariantKind::Struct(s) => { + let pattern = s + .fields + .iter() + .map(|field| field.name.rust_style().to_owned()) + .collect::>(); + let pattern = if s.is_fields_named { + format!("{}::{} {{ {} }}", src.name, variant.name, pattern.join(",")) + } else { + format!("{}::{}({})", src.name, variant.name, pattern.join(",")) + }; + let checks = s + .fields + .iter() + .map(|field| { + format!( + "let _: {} = {};\n", + field.ty.rust_api_type(), + field.name.rust_style(), + ) + }) + .collect::>(); + format!("{} => {{ {} }}", pattern, checks.join("")) + } + }) + .collect(); + Some(format!( + "match None::<{}>.unwrap() {{ {} }}", + src.name, + branches.join(","), + )) + } + + fn wrapper_struct(&self) -> Option { + let src = self.ir.get(self.context.ir_file); + src.wrapper_name.as_ref().cloned() + } + + fn self_access(&self, obj: String) -> String { + let src = self.ir.get(self.context.ir_file); + match &src.wrapper_name { + Some(_) => format!("{}.0", obj), + None => obj, + } + } + + fn wrap_obj(&self, obj: String) -> String { + match self.wrapper_struct() { + Some(wrapper) => format!("{}({})", wrapper, obj), + None => obj, + } + } + + fn impl_intodart(&self) -> String { + let src = self.ir.get(self.context.ir_file); + + let (name, self_path): (&str, &str) = match &src.wrapper_name { + Some(wrapper) => (wrapper, &src.name), + None => (&src.name, "Self"), + }; + let self_ref = self.self_access("self".to_owned()); + if self.ir.is_struct { + let variants = src + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| { + let tag = format!("{}.into_dart()", idx); + match &variant.kind { + IrVariantKind::Value => { + format!("{}::{} => vec![{}],", self_path, variant.name, tag) + } + IrVariantKind::Struct(s) => { + let fields = Some(tag) + .into_iter() + .chain(s.fields.iter().map(|field| { + let gen = TypeRustGenerator::new( + field.ty.clone(), + self.context.ir_file, + ); + gen.convert_to_dart(field.name.rust_style().to_owned()) + })) + .collect::>(); + let pattern = s + .fields + .iter() + .map(|field| field.name.rust_style().to_owned()) + .collect::>(); + let (left, right) = s.brackets_pair(); + format!( + "{}::{}{}{}{} => vec![{}],", + self_path, + variant.name, + left, + pattern.join(","), + right, + fields.join(",") + ) + } + } + }) + .collect::>(); + format!( + "impl support::IntoDart for {} {{ + fn into_dart(self) -> support::DartCObject {{ + match {} {{ + {} + }}.into_dart() + }} + }} + impl support::IntoDartExceptPrimitive for {0} {{}} + ", + name, + self_ref, + variants.join("\n") + ) + } else { + let variants = src + .variants() + .iter() + .enumerate() + .map(|(idx, variant)| format!("{}::{} => {},", self_path, variant.name, idx)) + .collect::>() + .join("\n"); + format!( + "impl support::IntoDart for {} {{ + fn into_dart(self) -> support::DartCObject {{ + match {} {{ + {} + }}.into_dart() + }} + }} + ", + name, self_ref, variants + ) + } + } + + fn new_with_nullptr(&self, collector: &mut ExternFuncCollector) -> String { + if !self.ir.is_struct { + return "".to_string(); + } + + fn init_of(ty: &IrType) -> &str { + if ty.rust_wire_is_pointer() { + "core::ptr::null_mut()" + } else { + "Default::default()" + } + } + + let src = self.ir.get(self.context.ir_file); + + let inflators = src + .variants() + .iter() + .filter_map(|variant| { + let typ = format!("{}_{}", self.ir.name, variant.name); + let body: Vec<_> = if let IrVariantKind::Struct(st) = &variant.kind { + st.fields + .iter() + .map(|field| format!("{}: {}", field.name.rust_style(), init_of(&field.ty))) + .collect() + } else { + return None; + }; + Some(collector.generate( + &format!("inflate_{}", typ), + &[], + Some(&format!("*mut {}Kind", self.ir.name)), + &format!( + "support::new_leak_box_ptr({}Kind {{ + {}: support::new_leak_box_ptr({} {{ + {} + }}) + }})", + self.ir.name, + variant.name.rust_style(), + typ, + body.join(",") + ), + )) + }) + .collect::>(); + format!( + "impl NewWithNullPtr for {} {{ + fn new_with_null_ptr() -> Self {{ + Self {{ + tag: -1, + kind: core::ptr::null_mut(), + }} + }} + }} + {}", + self.ir.rust_wire_type(), + inflators.join("\n\n") + ) + } + + fn imports(&self) -> Option { + let api_enum = self.ir.get(self.context.ir_file); + Some(format!("use {};", api_enum.path.join("::"))) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_general_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_general_list.rs new file mode 100644 index 000000000..1e88a5867 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_general_list.rs @@ -0,0 +1,55 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::{generate_import, generate_list_allocate_func, ExternFuncCollector}; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeGeneralListGenerator, IrTypeGeneralList); + +impl TypeGeneralListGenerator<'_> { + pub const WIRE2API_BODY: &'static str = " + let vec = unsafe { + let wrap = support::box_from_leak_ptr(self); + support::vec_from_leak_ptr(wrap.ptr, wrap.len) + }; + vec.into_iter().map(Wire2Api::wire2api).collect()"; +} + +impl TypeRustGeneratorTrait for TypeGeneralListGenerator<'_> { + fn wire2api_body(&self) -> Option { + Some(TypeGeneralListGenerator::WIRE2API_BODY.to_string()) + } + + fn wire_struct_fields(&self) -> Option> { + Some(vec![ + format!( + "ptr: *mut {}{}", + self.ir.inner.rust_ptr_modifier(), + self.ir.inner.rust_wire_type() + ), + "len: i32".to_string(), + ]) + } + + fn wrap_obj(&self, obj: String) -> String { + let inner = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); + inner + .wrapper_struct() + .map(|wrapper| { + format!( + "{}.into_iter().map(|v| {}({})).collect::>()", + obj, + wrapper, + inner.self_access("v".to_owned()) + ) + }) + .unwrap_or(obj) + } + + fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { + generate_list_allocate_func(collector, &self.ir.safe_ident(), &self.ir, &self.ir.inner) + } + + fn imports(&self) -> Option { + generate_import(&self.ir.inner, self.context.ir_file) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_optional.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_optional.rs new file mode 100644 index 000000000..4e13ee23c --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_optional.rs @@ -0,0 +1,30 @@ +use crate::generator::rust::generate_import; +use crate::generator::rust::ty::*; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeOptionalGenerator, IrTypeOptional); + +impl TypeRustGeneratorTrait for TypeOptionalGenerator<'_> { + fn wire2api_body(&self) -> Option { + None + } + + fn convert_to_dart(&self, obj: String) -> String { + let inner = TypeRustGenerator::new(*self.ir.inner.clone(), self.context.ir_file); + let obj = match inner.wrapper_struct() { + Some(wrapper) => format!( + "{}.map(|v| {}({}))", + obj, + wrapper, + inner.self_access("v".to_owned()) + ), + None => obj, + }; + format!("{}.into_dart()", obj) + } + + fn imports(&self) -> Option { + generate_import(&self.ir.inner, self.context.ir_file) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive.rs new file mode 100644 index 000000000..5fd3bb562 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive.rs @@ -0,0 +1,11 @@ +use crate::generator::rust::ty::*; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypePrimitiveGenerator, IrTypePrimitive); + +impl TypeRustGeneratorTrait for TypePrimitiveGenerator<'_> { + fn wire2api_body(&self) -> Option { + Some("self".into()) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive_list.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive_list.rs new file mode 100644 index 000000000..3fa85f82c --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_primitive_list.rs @@ -0,0 +1,42 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::ExternFuncCollector; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypePrimitiveListGenerator, IrTypePrimitiveList); + +impl TypeRustGeneratorTrait for TypePrimitiveListGenerator<'_> { + fn wire2api_body(&self) -> Option { + Some( + "unsafe { + let wrap = support::box_from_leak_ptr(self); + support::vec_from_leak_ptr(wrap.ptr, wrap.len) + }" + .into(), + ) + } + + fn wire_struct_fields(&self) -> Option> { + Some(vec![ + format!("ptr: *mut {}", self.ir.primitive.rust_wire_type()), + "len: i32".to_string(), + ]) + } + + fn allocate_funcs(&self, collector: &mut ExternFuncCollector) -> String { + collector.generate( + &format!("new_{}", self.ir.safe_ident()), + &["len: i32"], + Some(&format!( + "{}{}", + self.ir.rust_wire_modifier(), + self.ir.rust_wire_type() + )), + &format!( + "let ans = {} {{ ptr: support::new_leak_vec_ptr(Default::default(), len), len }}; + support::new_leak_box_ptr(ans)", + self.ir.rust_wire_type(), + ), + ) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_struct.rs b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_struct.rs new file mode 100644 index 000000000..021dd9f47 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/generator/rust/ty_struct.rs @@ -0,0 +1,185 @@ +use crate::generator::rust::ty::*; +use crate::generator::rust::ExternFuncCollector; +use crate::ir::*; +use crate::type_rust_generator_struct; + +type_rust_generator_struct!(TypeStructRefGenerator, IrTypeStructRef); + +impl TypeRustGeneratorTrait for TypeStructRefGenerator<'_> { + fn wire2api_body(&self) -> Option { + let api_struct = self.ir.get(self.context.ir_file); + let fields_str = &api_struct + .fields + .iter() + .map(|field| { + format!( + "{} self.{}.wire2api()", + if api_struct.is_fields_named { + field.name.rust_style().to_string() + ": " + } else { + String::new() + }, + field.name.rust_style() + ) + }) + .collect::>() + .join(","); + + let (left, right) = api_struct.brackets_pair(); + Some(format!( + "{}{}{}{}", + self.ir.rust_api_type(), + left, + fields_str, + right + )) + } + + fn wire_struct_fields(&self) -> Option> { + let s = self.ir.get(self.context.ir_file); + Some( + s.fields + .iter() + .map(|field| { + format!( + "{}: {}{}", + field.name.rust_style(), + field.ty.rust_wire_modifier(), + field.ty.rust_wire_type() + ) + }) + .collect(), + ) + } + + fn static_checks(&self) -> Option { + let src = self.ir.get(self.context.ir_file); + src.wrapper_name.as_ref()?; + + let var = if src.is_fields_named { + src.name.clone() + } else { + // let bindings cannot shadow tuple structs + format!("{}_", src.name) + }; + let checks = src + .fields + .iter() + .enumerate() + .map(|(i, field)| { + format!( + "let _: {} = {}.{};\n", + field.ty.rust_api_type(), + var, + if src.is_fields_named { + field.name.to_string() + } else { + i.to_string() + }, + ) + }) + .collect::>() + .join(""); + Some(format!( + "{{ let {} = None::<{}>.unwrap(); {} }} ", + var, src.name, checks + )) + } + + fn wrapper_struct(&self) -> Option { + let src = self.ir.get(self.context.ir_file); + src.wrapper_name.as_ref().cloned() + } + + fn wrap_obj(&self, obj: String) -> String { + match self.wrapper_struct() { + Some(wrapper) => format!("{}({})", wrapper, obj), + None => obj, + } + } + + fn impl_intodart(&self) -> String { + let src = self.ir.get(self.context.ir_file); + + let unwrap = match &src.wrapper_name { + Some(_) => ".0", + None => "", + }; + let body = src + .fields + .iter() + .enumerate() + .map(|(i, field)| { + let field_ref = if src.is_fields_named { + field.name.rust_style().to_string() + } else { + i.to_string() + }; + let gen = TypeRustGenerator::new(field.ty.clone(), self.context.ir_file); + gen.convert_to_dart(gen.wrap_obj(format!("self{}.{}", unwrap, field_ref))) + }) + .collect::>() + .join(",\n"); + + let name = match &src.wrapper_name { + Some(wrapper) => wrapper, + None => &src.name, + }; + format!( + "impl support::IntoDart for {} {{ + fn into_dart(self) -> support::DartCObject {{ + vec![ + {} + ].into_dart() + }} + }} + impl support::IntoDartExceptPrimitive for {} {{}} + ", + name, body, name, + ) + } + + fn new_with_nullptr(&self, _collector: &mut ExternFuncCollector) -> String { + let src = self.ir.get(self.context.ir_file); + + let body = { + src.fields + .iter() + .map(|field| { + format!( + "{}: {},", + field.name.rust_style(), + if field.ty.rust_wire_is_pointer() { + "core::ptr::null_mut()" + } else { + "Default::default()" + } + ) + }) + .collect::>() + .join("\n") + }; + format!( + r#"impl NewWithNullPtr for {} {{ + fn new_with_null_ptr() -> Self {{ + Self {{ {} }} + }} + }} + "#, + self.ir.rust_wire_type(), + body, + ) + } + + fn imports(&self) -> Option { + let api_struct = self.ir.get(self.context.ir_file); + if api_struct.path.is_some() { + Some(format!( + "use {};", + api_struct.path.as_ref().unwrap().join("::") + )) + } else { + None + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/annotation.rs b/libs/flutter_rust_bridge_codegen/src/ir/annotation.rs new file mode 100644 index 000000000..e67580142 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/annotation.rs @@ -0,0 +1,7 @@ +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrDartAnnotation { + pub content: String, + pub library: Option, +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/comment.rs b/libs/flutter_rust_bridge_codegen/src/ir/comment.rs new file mode 100644 index 000000000..e91af1602 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/comment.rs @@ -0,0 +1,26 @@ +#[derive(Debug, Clone)] +pub struct IrComment(String); + +impl IrComment { + pub fn comment(&self) -> &str { + &self.0 + } +} + +impl From<&str> for IrComment { + fn from(input: &str) -> Self { + if input.contains('\n') { + // Dart's formatter has issues with block comments + // so we convert them ahead of time. + let formatted = input + .split('\n') + .into_iter() + .map(|e| format!("///{}", e)) + .collect::>() + .join("\n"); + Self(formatted) + } else { + Self(format!("///{}", input)) + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/field.rs b/libs/flutter_rust_bridge_codegen/src/ir/field.rs new file mode 100644 index 000000000..8c54bd54e --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/field.rs @@ -0,0 +1,9 @@ +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrField { + pub ty: IrType, + pub name: IrIdent, + pub is_final: bool, + pub comments: Vec, +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/file.rs b/libs/flutter_rust_bridge_codegen/src/ir/file.rs new file mode 100644 index 000000000..cbfec6723 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/file.rs @@ -0,0 +1,61 @@ +use crate::ir::*; +use std::collections::{HashMap, HashSet}; + +pub type IrStructPool = HashMap; +pub type IrEnumPool = HashMap; + +#[derive(Debug, Clone)] +pub struct IrFile { + pub funcs: Vec, + pub struct_pool: IrStructPool, + pub enum_pool: IrEnumPool, + pub has_executor: bool, +} + +impl IrFile { + /// [f] returns [true] if it wants to stop going to the *children* of this subtree + pub fn visit_types bool>( + &self, + f: &mut F, + include_func_inputs: bool, + include_func_output: bool, + ) { + for func in &self.funcs { + if include_func_inputs { + for field in &func.inputs { + field.ty.visit_types(f, self); + } + } + if include_func_output { + func.output.visit_types(f, self); + } + } + } + + pub fn distinct_types( + &self, + include_func_inputs: bool, + include_func_output: bool, + ) -> Vec { + let mut seen_idents = HashSet::new(); + let mut ans = Vec::new(); + self.visit_types( + &mut |ty| { + let ident = ty.safe_ident(); + let contains = seen_idents.contains(&ident); + if !contains { + seen_idents.insert(ident); + ans.push(ty.clone()); + } + contains + }, + include_func_inputs, + include_func_output, + ); + + // make the output change less when input change + ans.sort_by_key(|ty| ty.safe_ident()); + + ans + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/func.rs b/libs/flutter_rust_bridge_codegen/src/ir/func.rs new file mode 100644 index 000000000..4bce1c42f --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/func.rs @@ -0,0 +1,60 @@ +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrFunc { + pub name: String, + pub inputs: Vec, + pub output: IrType, + pub fallible: bool, + pub mode: IrFuncMode, + pub comments: Vec, +} + +impl IrFunc { + pub fn wire_func_name(&self) -> String { + format!("wire_{}", self.name) + } +} + +/// Represents a function's output type +#[derive(Debug, Clone)] +pub enum IrFuncOutput { + ResultType(IrType), + Type(IrType), +} + +/// Represents the type of an argument to a function +#[derive(Debug, Clone)] +pub enum IrFuncArg { + StreamSinkType(IrType), + Type(IrType), +} + +#[derive(Debug, Clone, PartialOrd, PartialEq)] +pub enum IrFuncMode { + Normal, + Sync, + Stream, +} + +impl IrFuncMode { + pub fn dart_return_type(&self, inner: &str) -> String { + match self { + Self::Normal => format!("Future<{}>", inner), + Self::Sync => inner.to_string(), + Self::Stream => format!("Stream<{}>", inner), + } + } + + pub fn ffi_call_mode(&self) -> &'static str { + match self { + Self::Normal => "Normal", + Self::Sync => "Sync", + Self::Stream => "Stream", + } + } + + pub fn has_port_argument(&self) -> bool { + self != &Self::Sync + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ident.rs b/libs/flutter_rust_bridge_codegen/src/ir/ident.rs new file mode 100644 index 000000000..c86ac25fe --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ident.rs @@ -0,0 +1,26 @@ +use convert_case::{Case, Casing}; + +#[derive(Debug, Clone)] +pub struct IrIdent { + pub raw: String, +} + +impl std::fmt::Display for IrIdent { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + fmt.write_str(&self.raw) + } +} + +impl IrIdent { + pub fn new(raw: String) -> IrIdent { + IrIdent { raw } + } + + pub fn rust_style(&self) -> &str { + &self.raw + } + + pub fn dart_style(&self) -> String { + self.raw.to_case(Case::Camel) + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/import.rs b/libs/flutter_rust_bridge_codegen/src/ir/import.rs new file mode 100644 index 000000000..072975c35 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/import.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct IrDartImport { + pub uri: String, + pub alias: Option, +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/mod.rs b/libs/flutter_rust_bridge_codegen/src/ir/mod.rs new file mode 100644 index 000000000..eb3c73c47 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/mod.rs @@ -0,0 +1,33 @@ +mod annotation; +mod comment; +mod field; +mod file; +mod func; +mod ident; +mod import; +mod ty; +mod ty_boxed; +mod ty_delegate; +mod ty_enum; +mod ty_general_list; +mod ty_optional; +mod ty_primitive; +mod ty_primitive_list; +mod ty_struct; + +pub use annotation::*; +pub use comment::*; +pub use field::*; +pub use file::*; +pub use func::*; +pub use ident::*; +pub use import::*; +pub use ty::*; +pub use ty_boxed::*; +pub use ty_delegate::*; +pub use ty_enum::*; +pub use ty_general_list::*; +pub use ty_optional::*; +pub use ty_primitive::*; +pub use ty_primitive_list::*; +pub use ty_struct::*; diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty.rs new file mode 100644 index 000000000..d342c54c7 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty.rs @@ -0,0 +1,84 @@ +use crate::ir::*; +use enum_dispatch::enum_dispatch; +use IrType::*; + +/// Remark: "Ty" instead of "Type", since "type" is a reserved word in Rust. +#[enum_dispatch(IrTypeTrait)] +#[derive(Debug, Clone)] +pub enum IrType { + Primitive(IrTypePrimitive), + Delegate(IrTypeDelegate), + PrimitiveList(IrTypePrimitiveList), + Optional(IrTypeOptional), + GeneralList(IrTypeGeneralList), + StructRef(IrTypeStructRef), + Boxed(IrTypeBoxed), + EnumRef(IrTypeEnumRef), +} + +impl IrType { + pub fn visit_types bool>(&self, f: &mut F, ir_file: &IrFile) { + if f(self) { + return; + } + + self.visit_children_types(f, ir_file); + } + + #[inline] + pub fn dart_required_modifier(&self) -> &'static str { + match self { + Optional(_) => "", + _ => "required ", + } + } + + /// Additional indirection for types put behind a vector + #[inline] + pub fn rust_ptr_modifier(&self) -> &'static str { + match self { + Optional(_) | Delegate(IrTypeDelegate::String) => "*mut ", + _ => "", + } + } +} + +#[enum_dispatch] +pub trait IrTypeTrait { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile); + + fn safe_ident(&self) -> String; + + fn dart_api_type(&self) -> String; + + fn dart_wire_type(&self) -> String; + + fn rust_api_type(&self) -> String; + + fn rust_wire_type(&self) -> String; + + fn rust_wire_modifier(&self) -> String { + if self.rust_wire_is_pointer() { + "*mut ".to_string() + } else { + "".to_string() + } + } + + fn rust_wire_is_pointer(&self) -> bool { + false + } +} + +pub fn optional_boundary_index(types: &[&IrType]) -> Option { + types + .iter() + .enumerate() + .find(|ty| matches!(ty.1, Optional(_))) + .and_then(|(idx, _)| { + (&types[idx..]) + .iter() + .all(|ty| matches!(ty, Optional(_))) + .then(|| idx) + }) +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_boxed.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_boxed.rs new file mode 100644 index 000000000..0ef2cddb0 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_boxed.rs @@ -0,0 +1,56 @@ +use crate::ir::IrType::Primitive; +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrTypeBoxed { + /// if false, means that we automatically add it when transforming it - it does not exist in real api. + pub exist_in_real_api: bool, + pub inner: Box, +} + +impl IrTypeTrait for IrTypeBoxed { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + self.inner.visit_types(f, ir_file); + } + + fn safe_ident(&self) -> String { + format!( + "box_{}{}", + if self.exist_in_real_api { + "" + } else { + "autoadd_" + }, + self.inner.safe_ident() + ) + } + + fn dart_api_type(&self) -> String { + self.inner.dart_api_type() + } + + fn dart_wire_type(&self) -> String { + let wire_type = if let Primitive(prim) = &*self.inner { + prim.dart_native_type().to_owned() + } else { + self.inner.dart_wire_type() + }; + format!("ffi.Pointer<{}>", wire_type) + } + + fn rust_api_type(&self) -> String { + if self.exist_in_real_api { + format!("Box<{}>", self.inner.rust_api_type()) + } else { + self.inner.rust_api_type() + } + } + + fn rust_wire_type(&self) -> String { + self.inner.rust_wire_type() + } + + fn rust_wire_is_pointer(&self) -> bool { + true + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_delegate.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_delegate.rs new file mode 100644 index 000000000..ac2574e4f --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_delegate.rs @@ -0,0 +1,85 @@ +use crate::ir::*; + +/// types that delegate to another type +#[derive(Debug, Clone)] +pub enum IrTypeDelegate { + String, + StringList, + SyncReturnVecU8, + ZeroCopyBufferVecPrimitive(IrTypePrimitive), +} + +impl IrTypeDelegate { + pub fn get_delegate(&self) -> IrType { + match self { + IrTypeDelegate::String => IrType::PrimitiveList(IrTypePrimitiveList { + primitive: IrTypePrimitive::U8, + }), + IrTypeDelegate::SyncReturnVecU8 => IrType::PrimitiveList(IrTypePrimitiveList { + primitive: IrTypePrimitive::U8, + }), + IrTypeDelegate::ZeroCopyBufferVecPrimitive(primitive) => { + IrType::PrimitiveList(IrTypePrimitiveList { + primitive: primitive.clone(), + }) + } + IrTypeDelegate::StringList => IrType::Delegate(IrTypeDelegate::String), + } + } +} + +impl IrTypeTrait for IrTypeDelegate { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + self.get_delegate().visit_types(f, ir_file); + } + + fn safe_ident(&self) -> String { + match self { + IrTypeDelegate::String => "String".to_owned(), + IrTypeDelegate::StringList => "StringList".to_owned(), + IrTypeDelegate::SyncReturnVecU8 => "SyncReturnVecU8".to_owned(), + IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + "ZeroCopyBuffer_".to_owned() + &self.get_delegate().dart_api_type() + } + } + } + + fn dart_api_type(&self) -> String { + match self { + IrTypeDelegate::String => "String".to_string(), + IrTypeDelegate::StringList => "List".to_owned(), + IrTypeDelegate::SyncReturnVecU8 | IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + self.get_delegate().dart_api_type() + } + } + } + + fn dart_wire_type(&self) -> String { + match self { + IrTypeDelegate::StringList => "ffi.Pointer".to_owned(), + _ => self.get_delegate().dart_wire_type(), + } + } + + fn rust_api_type(&self) -> String { + match self { + IrTypeDelegate::String => "String".to_owned(), + IrTypeDelegate::SyncReturnVecU8 => "SyncReturn>".to_string(), + IrTypeDelegate::StringList => "Vec".to_owned(), + IrTypeDelegate::ZeroCopyBufferVecPrimitive(_) => { + format!("ZeroCopyBuffer<{}>", self.get_delegate().rust_api_type()) + } + } + } + + fn rust_wire_type(&self) -> String { + match self { + IrTypeDelegate::StringList => "wire_StringList".to_owned(), + _ => self.get_delegate().rust_wire_type(), + } + } + + fn rust_wire_is_pointer(&self) -> bool { + self.get_delegate().rust_wire_is_pointer() + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_enum.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_enum.rs new file mode 100644 index 000000000..bae45a692 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_enum.rs @@ -0,0 +1,139 @@ +use crate::ir::IrType::{EnumRef, StructRef}; +use crate::ir::*; +use convert_case::{Case, Casing}; + +#[derive(Debug, Clone)] +pub struct IrTypeEnumRef { + pub name: String, + pub is_struct: bool, +} + +impl IrTypeEnumRef { + pub fn get<'a>(&self, file: &'a IrFile) -> &'a IrEnum { + &file.enum_pool[&self.name] + } +} + +impl IrTypeTrait for IrTypeEnumRef { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + let enu = self.get(ir_file); + for variant in enu.variants() { + if let IrVariantKind::Struct(st) = &variant.kind { + st.fields + .iter() + .for_each(|field| field.ty.visit_types(f, ir_file)); + } + } + } + + fn safe_ident(&self) -> String { + self.dart_api_type().to_case(Case::Snake) + } + fn dart_api_type(&self) -> String { + self.name.to_string() + } + fn dart_wire_type(&self) -> String { + if self.is_struct { + self.rust_wire_type() + } else { + "int".to_owned() + } + } + fn rust_api_type(&self) -> String { + self.name.to_string() + } + fn rust_wire_type(&self) -> String { + if self.is_struct { + format!("wire_{}", self.name) + } else { + "i32".to_owned() + } + } +} + +#[derive(Debug, Clone)] +pub struct IrEnum { + pub name: String, + pub wrapper_name: Option, + pub path: Vec, + pub comments: Vec, + _variants: Vec, + _is_struct: bool, +} + +impl IrEnum { + pub fn new( + name: String, + wrapper_name: Option, + path: Vec, + comments: Vec, + mut variants: Vec, + ) -> Self { + fn wrap_box(ty: IrType) -> IrType { + match ty { + StructRef(_) + | EnumRef(IrTypeEnumRef { + is_struct: true, .. + }) => IrType::Boxed(IrTypeBoxed { + exist_in_real_api: false, + inner: Box::new(ty), + }), + _ => ty, + } + } + let _is_struct = variants + .iter() + .any(|variant| !matches!(variant.kind, IrVariantKind::Value)); + if _is_struct { + variants = variants + .into_iter() + .map(|variant| IrVariant { + kind: match variant.kind { + IrVariantKind::Struct(st) => IrVariantKind::Struct(IrStruct { + fields: st + .fields + .into_iter() + .map(|field| IrField { + ty: wrap_box(field.ty), + ..field + }) + .collect(), + ..st + }), + _ => variant.kind, + }, + ..variant + }) + .collect::>(); + } + Self { + name, + wrapper_name, + path, + comments, + _variants: variants, + _is_struct, + } + } + + pub fn variants(&self) -> &[IrVariant] { + &self._variants + } + + pub fn is_struct(&self) -> bool { + self._is_struct + } +} + +#[derive(Debug, Clone)] +pub struct IrVariant { + pub name: IrIdent, + pub comments: Vec, + pub kind: IrVariantKind, +} + +#[derive(Debug, Clone)] +pub enum IrVariantKind { + Value, + Struct(IrStruct), +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_general_list.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_general_list.rs new file mode 100644 index 000000000..44f5fde95 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_general_list.rs @@ -0,0 +1,36 @@ +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrTypeGeneralList { + pub inner: Box, +} + +impl IrTypeTrait for IrTypeGeneralList { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + self.inner.visit_types(f, ir_file); + } + + fn safe_ident(&self) -> String { + format!("list_{}", self.inner.safe_ident()) + } + + fn dart_api_type(&self) -> String { + format!("List<{}>", self.inner.dart_api_type()) + } + + fn dart_wire_type(&self) -> String { + format!("ffi.Pointer", self.safe_ident()) + } + + fn rust_api_type(&self) -> String { + format!("Vec<{}>", self.inner.rust_api_type()) + } + + fn rust_wire_type(&self) -> String { + format!("wire_{}", self.safe_ident()) + } + + fn rust_wire_is_pointer(&self) -> bool { + true + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_optional.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_optional.rs new file mode 100644 index 000000000..580788918 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_optional.rs @@ -0,0 +1,65 @@ +use crate::ir::IrType::*; +use crate::ir::*; + +#[derive(Debug, Clone)] +pub struct IrTypeOptional { + pub inner: Box, +} + +impl IrTypeOptional { + pub fn new_prim(prim: IrTypePrimitive) -> Self { + Self { + inner: Box::new(Boxed(IrTypeBoxed { + inner: Box::new(Primitive(prim)), + exist_in_real_api: false, + })), + } + } + + pub fn new_ptr(ptr: IrType) -> Self { + Self { + inner: Box::new(ptr), + } + } + + pub fn is_primitive(&self) -> bool { + matches!(&*self.inner, Boxed(boxed) if matches!(*boxed.inner, IrType::Primitive(_))) + } + + pub fn is_list(&self) -> bool { + matches!(&*self.inner, GeneralList(_) | PrimitiveList(_)) + } + + pub fn is_delegate(&self) -> bool { + matches!(&*self.inner, Delegate(_)) + } + + pub fn needs_initialization(&self) -> bool { + !(self.is_primitive() || self.is_delegate()) + } +} + +impl IrTypeTrait for IrTypeOptional { + fn safe_ident(&self) -> String { + format!("opt_{}", self.inner.safe_ident()) + } + fn rust_wire_type(&self) -> String { + self.inner.rust_wire_type() + } + fn rust_api_type(&self) -> String { + format!("Option<{}>", self.inner.rust_api_type()) + } + fn dart_wire_type(&self) -> String { + self.inner.dart_wire_type() + } + fn dart_api_type(&self) -> String { + format!("{}?", self.inner.dart_api_type()) + } + fn rust_wire_is_pointer(&self) -> bool { + true + } + + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + self.inner.visit_types(f, ir_file); + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive.rs new file mode 100644 index 000000000..06cfece9d --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive.rs @@ -0,0 +1,114 @@ +use crate::ir::*; + +#[derive(Debug, Clone)] +pub enum IrTypePrimitive { + U8, + I8, + U16, + I16, + U32, + I32, + U64, + I64, + F32, + F64, + Bool, + Unit, + Usize, +} + +impl IrTypeTrait for IrTypePrimitive { + fn visit_children_types bool>(&self, _f: &mut F, _ir_file: &IrFile) {} + + fn safe_ident(&self) -> String { + self.rust_api_type() + } + + fn dart_api_type(&self) -> String { + match self { + IrTypePrimitive::U8 + | IrTypePrimitive::I8 + | IrTypePrimitive::U16 + | IrTypePrimitive::I16 + | IrTypePrimitive::U32 + | IrTypePrimitive::I32 + | IrTypePrimitive::U64 + | IrTypePrimitive::I64 + | IrTypePrimitive::Usize => "int", + IrTypePrimitive::F32 | IrTypePrimitive::F64 => "double", + IrTypePrimitive::Bool => "bool", + IrTypePrimitive::Unit => "void", + } + .to_string() + } + + fn dart_wire_type(&self) -> String { + match self { + IrTypePrimitive::Bool => "int".to_owned(), + _ => self.dart_api_type(), + } + } + + fn rust_api_type(&self) -> String { + self.rust_wire_type() + } + + fn rust_wire_type(&self) -> String { + match self { + IrTypePrimitive::U8 => "u8", + IrTypePrimitive::I8 => "i8", + IrTypePrimitive::U16 => "u16", + IrTypePrimitive::I16 => "i16", + IrTypePrimitive::U32 => "u32", + IrTypePrimitive::I32 => "i32", + IrTypePrimitive::U64 => "u64", + IrTypePrimitive::I64 => "i64", + IrTypePrimitive::F32 => "f32", + IrTypePrimitive::F64 => "f64", + IrTypePrimitive::Bool => "bool", + IrTypePrimitive::Unit => "unit", + IrTypePrimitive::Usize => "usize", + } + .to_string() + } +} + +impl IrTypePrimitive { + /// Representations of primitives within Dart's pointers, e.g. `ffi.Pointer`. + /// This is enforced on Dart's side, and should be used instead of `dart_wire_type` + /// whenever primitives are put behind a pointer. + pub fn dart_native_type(&self) -> &'static str { + match self { + IrTypePrimitive::U8 | IrTypePrimitive::Bool => "ffi.Uint8", + IrTypePrimitive::I8 => "ffi.Int8", + IrTypePrimitive::U16 => "ffi.Uint16", + IrTypePrimitive::I16 => "ffi.Int16", + IrTypePrimitive::U32 => "ffi.Uint32", + IrTypePrimitive::I32 => "ffi.Int32", + IrTypePrimitive::U64 => "ffi.Uint64", + IrTypePrimitive::I64 => "ffi.Int64", + IrTypePrimitive::F32 => "ffi.Float", + IrTypePrimitive::F64 => "ffi.Double", + IrTypePrimitive::Unit => "ffi.Void", + IrTypePrimitive::Usize => "ffi.Usize", + } + } + pub fn try_from_rust_str(s: &str) -> Option { + match s { + "u8" => Some(IrTypePrimitive::U8), + "i8" => Some(IrTypePrimitive::I8), + "u16" => Some(IrTypePrimitive::U16), + "i16" => Some(IrTypePrimitive::I16), + "u32" => Some(IrTypePrimitive::U32), + "i32" => Some(IrTypePrimitive::I32), + "u64" => Some(IrTypePrimitive::U64), + "i64" => Some(IrTypePrimitive::I64), + "f32" => Some(IrTypePrimitive::F32), + "f64" => Some(IrTypePrimitive::F64), + "bool" => Some(IrTypePrimitive::Bool), + "()" => Some(IrTypePrimitive::Unit), + "usize" => Some(IrTypePrimitive::Usize), + _ => None, + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive_list.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive_list.rs new file mode 100644 index 000000000..759d29d71 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_primitive_list.rs @@ -0,0 +1,50 @@ +use crate::ir::*; +use convert_case::{Case, Casing}; + +#[derive(Debug, Clone)] +pub struct IrTypePrimitiveList { + pub primitive: IrTypePrimitive, +} + +impl IrTypeTrait for IrTypePrimitiveList { + fn visit_children_types bool>(&self, f: &mut F, _ir_file: &IrFile) { + f(&IrType::Primitive(self.primitive.clone())); + } + + fn safe_ident(&self) -> String { + self.dart_api_type().to_case(Case::Snake) + } + + fn dart_api_type(&self) -> String { + match &self.primitive { + IrTypePrimitive::U8 => "Uint8List", + IrTypePrimitive::I8 => "Int8List", + IrTypePrimitive::U16 => "Uint16List", + IrTypePrimitive::I16 => "Int16List", + IrTypePrimitive::U32 => "Uint32List", + IrTypePrimitive::I32 => "Int32List", + IrTypePrimitive::U64 => "Uint64List", + IrTypePrimitive::I64 => "Int64List", + IrTypePrimitive::F32 => "Float32List", + IrTypePrimitive::F64 => "Float64List", + _ => panic!("does not support {:?} yet", &self.primitive), + } + .to_string() + } + + fn dart_wire_type(&self) -> String { + format!("ffi.Pointer", self.safe_ident()) + } + + fn rust_api_type(&self) -> String { + format!("Vec<{}>", self.primitive.rust_api_type()) + } + + fn rust_wire_type(&self) -> String { + format!("wire_{}", self.safe_ident()) + } + + fn rust_wire_is_pointer(&self) -> bool { + true + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/ir/ty_struct.rs b/libs/flutter_rust_bridge_codegen/src/ir/ty_struct.rs new file mode 100644 index 000000000..3cacbd626 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/ir/ty_struct.rs @@ -0,0 +1,66 @@ +use crate::ir::*; +use convert_case::{Case, Casing}; + +#[derive(Debug, Clone)] +pub struct IrTypeStructRef { + pub name: String, + pub freezed: bool, +} + +impl IrTypeStructRef { + pub fn get<'a>(&self, f: &'a IrFile) -> &'a IrStruct { + &f.struct_pool[&self.name] + } +} + +impl IrTypeTrait for IrTypeStructRef { + fn visit_children_types bool>(&self, f: &mut F, ir_file: &IrFile) { + for field in &self.get(ir_file).fields { + field.ty.visit_types(f, ir_file); + } + } + + fn safe_ident(&self) -> String { + self.dart_api_type().to_case(Case::Snake) + } + fn dart_api_type(&self) -> String { + self.name.to_string() + } + + fn dart_wire_type(&self) -> String { + self.rust_wire_type() + } + + fn rust_api_type(&self) -> String { + self.name.to_string() + } + + fn rust_wire_type(&self) -> String { + format!("wire_{}", self.name) + } +} + +#[derive(Debug, Clone)] +pub struct IrStruct { + pub name: String, + pub wrapper_name: Option, + pub path: Option>, + pub fields: Vec, + pub is_fields_named: bool, + pub dart_metadata: Vec, + pub comments: Vec, +} + +impl IrStruct { + pub fn brackets_pair(&self) -> (char, char) { + if self.is_fields_named { + ('{', '}') + } else { + ('(', ')') + } + } + + pub fn using_freezed(&self) -> bool { + self.dart_metadata.iter().any(|it| it.content == "freezed") + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/lib.rs b/libs/flutter_rust_bridge_codegen/src/lib.rs new file mode 100644 index 000000000..0eaa76529 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/lib.rs @@ -0,0 +1,183 @@ +use std::fs; +use std::path::Path; + +use log::info; +use pathdiff::diff_paths; + +use crate::commands::ensure_tools_available; +pub use crate::config::RawOpts as Opts; +use crate::ir::*; +use crate::others::*; +use crate::utils::*; + +mod commands; +mod config; +mod error; +mod generator; +mod ir; +mod markers; +mod others; +mod parser; +mod source_graph; +mod transformer; +mod utils; +use error::*; + +pub fn frb_codegen(raw_opts: Opts) -> anyhow::Result<()> { + ensure_tools_available()?; + + let config = config::parse(raw_opts); + info!("Picked config: {:?}", &config); + + let rust_output_dir = Path::new(&config.rust_output_path).parent().unwrap(); + let dart_output_dir = Path::new(&config.dart_output_path).parent().unwrap(); + + info!("Phase: Parse source code to AST"); + let source_rust_content = fs::read_to_string(&config.rust_input_path)?; + let file_ast = syn::parse_file(&source_rust_content)?; + + info!("Phase: Parse AST to IR"); + let raw_ir_file = parser::parse(&source_rust_content, file_ast, &config.manifest_path); + + info!("Phase: Transform IR"); + let ir_file = transformer::transform(raw_ir_file); + + info!("Phase: Generate Rust code"); + let generated_rust = generator::rust::generate( + &ir_file, + &mod_from_rust_path(&config.rust_input_path, &config.rust_crate_dir), + ); + fs::create_dir_all(&rust_output_dir)?; + fs::write(&config.rust_output_path, generated_rust.code)?; + + info!("Phase: Generate Dart code"); + let (generated_dart, needs_freezed) = generator::dart::generate( + &ir_file, + &config.dart_api_class_name(), + &config.dart_api_impl_class_name(), + &config.dart_wire_class_name(), + config + .dart_output_path_name() + .ok_or_else(|| Error::str("Invalid dart_output_path_name"))?, + ); + + info!("Phase: Other things"); + + commands::format_rust(&config.rust_output_path)?; + + if !config.skip_add_mod_to_lib { + others::try_add_mod_to_lib(&config.rust_crate_dir, &config.rust_output_path); + } + + let c_struct_names = ir_file + .distinct_types(true, true) + .iter() + .filter_map(|ty| { + if let IrType::StructRef(_) = ty { + Some(ty.rust_wire_type()) + } else { + None + } + }) + .collect(); + + let temp_dart_wire_file = tempfile::NamedTempFile::new()?; + let temp_bindgen_c_output_file = tempfile::Builder::new().suffix(".h").tempfile()?; + with_changed_file( + &config.rust_output_path, + DUMMY_WIRE_CODE_FOR_BINDGEN, + || { + commands::bindgen_rust_to_dart( + &config.rust_crate_dir, + temp_bindgen_c_output_file + .path() + .as_os_str() + .to_str() + .unwrap(), + temp_dart_wire_file.path().as_os_str().to_str().unwrap(), + &config.dart_wire_class_name(), + c_struct_names, + &config.llvm_path[..], + &config.llvm_compiler_opts, + ) + }, + )?; + + let effective_func_names = [ + generated_rust.extern_func_names, + EXTRA_EXTERN_FUNC_NAMES.to_vec(), + ] + .concat(); + let c_dummy_code = generator::c::generate_dummy(&effective_func_names); + for output in &config.c_output_path { + fs::create_dir_all(Path::new(output).parent().unwrap())?; + fs::write( + &output, + fs::read_to_string(&temp_bindgen_c_output_file)? + "\n" + &c_dummy_code, + )?; + } + + fs::create_dir_all(&dart_output_dir)?; + let generated_dart_wire_code_raw = fs::read_to_string(temp_dart_wire_file)?; + let generated_dart_wire = extract_dart_wire_content(&modify_dart_wire_content( + &generated_dart_wire_code_raw, + &config.dart_wire_class_name(), + )); + + sanity_check(&generated_dart_wire.body, &config.dart_wire_class_name())?; + + let generated_dart_decl_all = generated_dart.decl_code; + let generated_dart_impl_all = &generated_dart.impl_code + &generated_dart_wire; + if let Some(dart_decl_output_path) = &config.dart_decl_output_path { + let impl_import_decl = DartBasicCode { + import: format!( + "import \"{}\";", + diff_paths(dart_decl_output_path, dart_output_dir) + .unwrap() + .to_str() + .unwrap() + ), + part: String::new(), + body: String::new(), + }; + fs::write( + &dart_decl_output_path, + (&generated_dart.file_prelude + &generated_dart_decl_all).to_text(), + )?; + fs::write( + &config.dart_output_path, + (&generated_dart.file_prelude + &impl_import_decl + &generated_dart_impl_all).to_text(), + )?; + } else { + fs::write( + &config.dart_output_path, + (&generated_dart.file_prelude + &generated_dart_decl_all + &generated_dart_impl_all) + .to_text(), + )?; + } + + let dart_root = &config.dart_root; + if needs_freezed && config.build_runner { + let dart_root = dart_root.as_ref().ok_or_else(|| { + Error::str( + "build_runner configured to run, but Dart root could not be inferred. + Please specify --dart-root, or disable build_runner with --no-build-runner.", + ) + })?; + commands::build_runner(dart_root)?; + commands::format_dart( + &config + .dart_output_freezed_path() + .ok_or_else(|| Error::str("Invalid freezed file path"))?, + config.dart_format_line_length, + )?; + } + + commands::format_dart(&config.dart_output_path, config.dart_format_line_length)?; + if let Some(dart_decl_output_path) = &config.dart_decl_output_path { + commands::format_dart(dart_decl_output_path, config.dart_format_line_length)?; + } + + info!("Success!"); + Ok(()) +} diff --git a/libs/flutter_rust_bridge_codegen/src/main.rs b/libs/flutter_rust_bridge_codegen/src/main.rs new file mode 100644 index 000000000..986ca3215 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/main.rs @@ -0,0 +1,19 @@ +use env_logger::Env; +use log::info; +use structopt::StructOpt; + +use lib_flutter_rust_bridge_codegen::{frb_codegen, Opts}; + +fn main() { + let opts = Opts::from_args(); + env_logger::Builder::from_env(Env::default().default_filter_or(if opts.verbose { + "debug" + } else { + "info" + })) + .init(); + + frb_codegen(opts).unwrap(); + + info!("Now go and use it :)"); +} diff --git a/libs/flutter_rust_bridge_codegen/src/markers.rs b/libs/flutter_rust_bridge_codegen/src/markers.rs new file mode 100644 index 000000000..048cb77db --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/markers.rs @@ -0,0 +1,39 @@ +use syn::*; + +/// Extract a path from marker `#[frb(mirror(path), ..)]` +pub fn extract_mirror_marker(attrs: &[Attribute]) -> Option { + attrs + .iter() + .filter(|attr| attr.path.is_ident("frb")) + .find_map(|attr| match attr.parse_meta() { + Ok(Meta::List(MetaList { nested, .. })) => nested.iter().find_map(|meta| match meta { + NestedMeta::Meta(Meta::List(MetaList { + path, + nested: mirror, + .. + })) if path.is_ident("mirror") && mirror.len() == 1 => { + match mirror.first().unwrap() { + NestedMeta::Meta(Meta::Path(path)) => Some(path.clone()), + _ => None, + } + } + _ => None, + }), + _ => None, + }) +} + +/// Checks if the `#[frb(non_final)]` attribute is present. +pub fn has_non_final(attrs: &[Attribute]) -> bool { + attrs + .iter() + .filter(|attr| attr.path.is_ident("frb")) + .any(|attr| { + match attr.parse_meta() { + Ok(Meta::List(MetaList { nested, .. })) => nested.iter().any(|meta| { + matches!(meta, NestedMeta::Meta(Meta::Path(path)) if path.is_ident("non_final")) + }), + _ => false, + } + }) +} diff --git a/libs/flutter_rust_bridge_codegen/src/others.rs b/libs/flutter_rust_bridge_codegen/src/others.rs new file mode 100644 index 000000000..4a8d10c8f --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/others.rs @@ -0,0 +1,169 @@ +use std::fs; +use std::ops::Add; +use std::path::Path; + +use anyhow::{anyhow, Result}; +use lazy_static::lazy_static; +use log::{info, warn}; +use pathdiff::diff_paths; +use regex::RegexBuilder; + +// NOTE [DartPostCObjectFnType] was originally [*mut DartCObject] but I changed it to [*mut c_void] +// because cannot automatically generate things related to [DartCObject]. Anyway this works fine. +// NOTE please sync [DUMMY_WIRE_CODE_FOR_BINDGEN] and [EXTRA_EXTERN_FUNC_NAMES] +pub const DUMMY_WIRE_CODE_FOR_BINDGEN: &str = r#" + // ----------- DUMMY CODE FOR BINDGEN ---------- + + // copied from: allo-isolate + pub type DartPort = i64; + pub type DartPostCObjectFnType = unsafe extern "C" fn(port_id: DartPort, message: *mut std::ffi::c_void) -> bool; + #[no_mangle] pub unsafe extern "C" fn store_dart_post_cobject(ptr: DartPostCObjectFnType) { panic!("dummy code") } + + // copied from: frb_rust::support.rs + #[repr(C)] + pub struct WireSyncReturnStruct { + pub ptr: *mut u8, + pub len: i32, + pub success: bool, + } + + // --------------------------------------------- + "#; + +lazy_static! { + pub static ref EXTRA_EXTERN_FUNC_NAMES: Vec = + vec!["store_dart_post_cobject".to_string()]; +} + +pub const CODE_HEADER: &str = "// AUTO GENERATED FILE, DO NOT EDIT. +// Generated by `flutter_rust_bridge`."; + +pub fn modify_dart_wire_content(content_raw: &str, dart_wire_class_name: &str) -> String { + let content = content_raw.replace( + &format!("class {} {{", dart_wire_class_name), + &format!( + "class {} implements FlutterRustBridgeWireBase {{", + dart_wire_class_name + ), + ); + + let content = RegexBuilder::new("class WireSyncReturnStruct extends ffi.Struct \\{.+?\\}") + .multi_line(true) + .dot_matches_new_line(true) + .build() + .unwrap() + .replace(&content, ""); + + content.to_string() +} + +#[derive(Default)] +pub struct DartBasicCode { + pub import: String, + pub part: String, + pub body: String, +} + +impl Add for &DartBasicCode { + type Output = DartBasicCode; + + fn add(self, rhs: Self) -> Self::Output { + DartBasicCode { + import: format!("{}\n{}", self.import, rhs.import), + part: format!("{}\n{}", self.part, rhs.part), + body: format!("{}\n{}", self.body, rhs.body), + } + } +} + +impl Add<&DartBasicCode> for DartBasicCode { + type Output = DartBasicCode; + + fn add(self, rhs: &DartBasicCode) -> Self::Output { + (&self).add(rhs) + } +} + +impl DartBasicCode { + pub fn to_text(&self) -> String { + format!("{}\n{}\n{}", self.import, self.part, self.body) + } +} + +pub fn extract_dart_wire_content(content: &str) -> DartBasicCode { + let (mut imports, mut body) = (Vec::new(), Vec::new()); + for line in content.split('\n') { + (if line.starts_with("import ") { + &mut imports + } else { + &mut body + }) + .push(line); + } + DartBasicCode { + import: imports.join("\n"), + part: "".to_string(), + body: body.join("\n"), + } +} + +pub fn sanity_check( + generated_dart_wire_code: &str, + dart_wire_class_name: &str, +) -> anyhow::Result<()> { + if !generated_dart_wire_code.contains(dart_wire_class_name) { + return Err(crate::error::Error::str( + "Nothing is generated for dart wire class. \ + Maybe you forget to put code like `mod the_generated_bridge_code;` to your `lib.rs`?", + ) + .into()); + } + Ok(()) +} + +pub fn try_add_mod_to_lib(rust_crate_dir: &str, rust_output_path: &str) { + if let Err(e) = auto_add_mod_to_lib_core(rust_crate_dir, rust_output_path) { + warn!( + "auto_add_mod_to_lib fail, the generated code may or may not have problems. \ + Please ensure you have add code like `mod the_generated_bridge_code;` to your `lib.rs`. \ + Details: {}", + e + ); + } +} + +pub fn auto_add_mod_to_lib_core(rust_crate_dir: &str, rust_output_path: &str) -> Result<()> { + let path_src_folder = Path::new(rust_crate_dir).join("src"); + let rust_output_path_relative_to_src_folder = + diff_paths(rust_output_path, path_src_folder.clone()).ok_or_else(|| { + anyhow!( + "rust_output_path={} is unrelated to path_src_folder={:?}", + rust_output_path, + &path_src_folder, + ) + })?; + + let mod_name = rust_output_path_relative_to_src_folder + .file_stem() + .ok_or_else(|| anyhow!(""))? + .to_str() + .ok_or_else(|| anyhow!(""))? + .to_string() + .replace('/', "::"); + let expect_code = format!("mod {};", mod_name); + + let path_lib_rs = path_src_folder.join("lib.rs"); + + let raw_content_lib_rs = fs::read_to_string(path_lib_rs.clone())?; + if !raw_content_lib_rs.contains(&expect_code) { + info!("Inject `{}` into {:?}", &expect_code, &path_lib_rs); + + let comments = " /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */"; + let modified_content_lib_rs = + format!("{}{}\n{}", expect_code, comments, raw_content_lib_rs); + + fs::write(&path_lib_rs, modified_content_lib_rs).unwrap(); + } + + Ok(()) +} diff --git a/libs/flutter_rust_bridge_codegen/src/parser/mod.rs b/libs/flutter_rust_bridge_codegen/src/parser/mod.rs new file mode 100644 index 000000000..16de7dd8e --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/parser/mod.rs @@ -0,0 +1,353 @@ +mod ty; + +use std::string::String; + +use log::debug; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::*; + +use crate::ir::*; + +use crate::generator::rust::HANDLER_NAME; +use crate::parser::ty::TypeParser; +use crate::source_graph::Crate; + +const STREAM_SINK_IDENT: &str = "StreamSink"; +const RESULT_IDENT: &str = "Result"; + +pub fn parse(source_rust_content: &str, file: File, manifest_path: &str) -> IrFile { + let crate_map = Crate::new(manifest_path); + + let src_fns = extract_fns_from_file(&file); + let src_structs = crate_map.root_module.collect_structs_to_vec(); + let src_enums = crate_map.root_module.collect_enums_to_vec(); + + let parser = Parser::new(TypeParser::new(src_structs, src_enums)); + parser.parse(source_rust_content, src_fns) +} + +struct Parser<'a> { + type_parser: TypeParser<'a>, +} + +impl<'a> Parser<'a> { + pub fn new(type_parser: TypeParser<'a>) -> Self { + Parser { type_parser } + } +} + +impl<'a> Parser<'a> { + fn parse(mut self, source_rust_content: &str, src_fns: Vec<&ItemFn>) -> IrFile { + let funcs = src_fns.iter().map(|f| self.parse_function(f)).collect(); + + let has_executor = source_rust_content.contains(HANDLER_NAME); + + let (struct_pool, enum_pool) = self.type_parser.consume(); + + IrFile { + funcs, + struct_pool, + enum_pool, + has_executor, + } + } + + /// Attempts to parse the type from the return part of a function signature. There is a special + /// case for top-level `Result` types. + pub fn try_parse_fn_output_type(&mut self, ty: &syn::Type) -> Option { + let inner = ty::SupportedInnerType::try_from_syn_type(ty)?; + + match inner { + ty::SupportedInnerType::Path(ty::SupportedPathType { + ident, + generic: Some(generic), + }) if ident == RESULT_IDENT => Some(IrFuncOutput::ResultType( + self.type_parser.convert_to_ir_type(*generic)?, + )), + _ => Some(IrFuncOutput::Type( + self.type_parser.convert_to_ir_type(inner)?, + )), + } + } + + /// Attempts to parse the type from an argument of a function signature. There is a special + /// case for top-level `StreamSink` types. + pub fn try_parse_fn_arg_type(&mut self, ty: &syn::Type) -> Option { + match ty { + syn::Type::Path(syn::TypePath { path, .. }) => { + let last_segment = path.segments.last().unwrap(); + if last_segment.ident == STREAM_SINK_IDENT { + match &last_segment.arguments { + syn::PathArguments::AngleBracketed( + syn::AngleBracketedGenericArguments { args, .. }, + ) if args.len() == 1 => { + // Unwrap is safe here because args.len() == 1 + match args.last().unwrap() { + syn::GenericArgument::Type(t) => { + Some(IrFuncArg::StreamSinkType(self.type_parser.parse_type(t))) + } + _ => None, + } + } + _ => None, + } + } else { + Some(IrFuncArg::Type(self.type_parser.parse_type(ty))) + } + } + _ => None, + } + } + + fn parse_function(&mut self, func: &ItemFn) -> IrFunc { + debug!("parse_function function name: {:?}", func.sig.ident); + + let sig = &func.sig; + let func_name = sig.ident.to_string(); + + let mut inputs = Vec::new(); + let mut output = None; + let mut mode = None; + let mut fallible = true; + + for sig_input in &sig.inputs { + if let FnArg::Typed(ref pat_type) = sig_input { + let name = if let Pat::Ident(ref pat_ident) = *pat_type.pat { + format!("{}", pat_ident.ident) + } else { + panic!("unexpected pat_type={:?}", pat_type) + }; + + match self.try_parse_fn_arg_type(&pat_type.ty).unwrap_or_else(|| { + panic!( + "Failed to parse function argument type `{}`", + type_to_string(&pat_type.ty) + ) + }) { + IrFuncArg::StreamSinkType(ty) => { + output = Some(ty); + mode = Some(IrFuncMode::Stream); + } + IrFuncArg::Type(ty) => { + inputs.push(IrField { + name: IrIdent::new(name), + ty, + is_final: true, + comments: extract_comments(&pat_type.attrs), + }); + } + } + } else { + panic!("unexpected sig_input={:?}", sig_input); + } + } + + if output.is_none() { + output = Some(match &sig.output { + ReturnType::Type(_, ty) => { + match self.try_parse_fn_output_type(ty).unwrap_or_else(|| { + panic!( + "Failed to parse function output type `{}`", + type_to_string(ty) + ) + }) { + IrFuncOutput::ResultType(ty) => ty, + IrFuncOutput::Type(ty) => { + fallible = false; + ty + } + } + } + ReturnType::Default => { + fallible = false; + IrType::Primitive(IrTypePrimitive::Unit) + } + }); + mode = Some( + if let Some(IrType::Delegate(IrTypeDelegate::SyncReturnVecU8)) = output { + IrFuncMode::Sync + } else { + IrFuncMode::Normal + }, + ); + } + + // let comments = func.attrs.iter().filter_map(extract_comments).collect(); + + IrFunc { + name: func_name, + inputs, + output: output.expect("unsupported output"), + fallible, + mode: mode.expect("unsupported mode"), + comments: extract_comments(&func.attrs), + } + } +} + +fn extract_fns_from_file(file: &File) -> Vec<&ItemFn> { + let mut src_fns = Vec::new(); + + for item in file.items.iter() { + if let Item::Fn(ref item_fn) = item { + if let Visibility::Public(_) = &item_fn.vis { + src_fns.push(item_fn); + } + } + } + + src_fns +} + +fn extract_comments(attrs: &[Attribute]) -> Vec { + attrs + .iter() + .filter_map(|attr| match attr.parse_meta() { + Ok(Meta::NameValue(MetaNameValue { + path, + lit: Lit::Str(lit), + .. + })) if path.is_ident("doc") => Some(IrComment::from(lit.value().as_ref())), + _ => None, + }) + .collect() +} + +pub mod frb_keyword { + syn::custom_keyword!(mirror); + syn::custom_keyword!(non_final); + syn::custom_keyword!(dart_metadata); + syn::custom_keyword!(import); +} + +#[derive(Clone, Debug)] +pub struct NamedOption { + pub name: K, + pub value: V, +} + +impl Parse for NamedOption { + fn parse(input: ParseStream<'_>) -> Result { + let name: K = input.parse()?; + let _: Token![=] = input.parse()?; + let value = input.parse()?; + Ok(Self { name, value }) + } +} + +#[derive(Clone, Debug)] +pub struct MirrorOption(Path); + +impl Parse for MirrorOption { + fn parse(input: ParseStream<'_>) -> Result { + let content; + parenthesized!(content in input); + let path: Path = content.parse()?; + Ok(Self(path)) + } +} + +#[derive(Clone, Debug)] +pub struct MetadataAnnotations(Vec); + +impl Parse for IrDartAnnotation { + fn parse(input: ParseStream<'_>) -> Result { + let annotation: LitStr = input.parse()?; + let library = if input.peek(frb_keyword::import) { + let _ = input.parse::()?; + let library: IrDartImport = input.parse()?; + Some(library) + } else { + None + }; + Ok(Self { + content: annotation.value(), + library, + }) + } +} +impl Parse for MetadataAnnotations { + fn parse(input: ParseStream<'_>) -> Result { + let content; + parenthesized!(content in input); + let annotations = + Punctuated::::parse_terminated(&content)? + .into_iter() + .collect(); + Ok(Self(annotations)) + } +} + +#[derive(Clone, Debug)] +pub struct DartImports(Vec); + +impl Parse for IrDartImport { + fn parse(input: ParseStream<'_>) -> Result { + let uri: LitStr = input.parse()?; + let alias: Option = if input.peek(token::As) { + let _ = input.parse::()?; + let alias: Ident = input.parse()?; + Some(alias.to_string()) + } else { + None + }; + Ok(Self { + uri: uri.value(), + alias, + }) + } +} +impl Parse for DartImports { + fn parse(input: ParseStream<'_>) -> Result { + let content; + parenthesized!(content in input); + let imports = Punctuated::::parse_terminated(&content)? + .into_iter() + .collect(); + Ok(Self(imports)) + } +} + +enum FrbOption { + Mirror(MirrorOption), + NonFinal, + Metadata(NamedOption), +} + +impl Parse for FrbOption { + fn parse(input: ParseStream<'_>) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(frb_keyword::mirror) { + input.parse().map(FrbOption::Mirror) + } else if lookahead.peek(frb_keyword::non_final) { + input + .parse::() + .map(|_| FrbOption::NonFinal) + } else if lookahead.peek(frb_keyword::dart_metadata) { + input.parse().map(FrbOption::Metadata) + } else { + Err(lookahead.error()) + } + } +} +fn extract_metadata(attrs: &[Attribute]) -> Vec { + attrs + .iter() + .filter(|attr| attr.path.is_ident("frb")) + .map(|attr| attr.parse_args::()) + .flat_map(|frb_option| match frb_option { + Ok(FrbOption::Metadata(NamedOption { + name: _, + value: MetadataAnnotations(annotations), + })) => annotations, + _ => vec![], + }) + .collect() +} + +/// syn -> string https://github.com/dtolnay/syn/issues/294 +fn type_to_string(ty: &Type) -> String { + quote!(#ty).to_string().replace(' ', "") +} diff --git a/libs/flutter_rust_bridge_codegen/src/parser/ty.rs b/libs/flutter_rust_bridge_codegen/src/parser/ty.rs new file mode 100644 index 000000000..15cbbce43 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/parser/ty.rs @@ -0,0 +1,392 @@ +use std::collections::{HashMap, HashSet}; +use std::string::String; + +use syn::*; + +use crate::ir::IrType::*; +use crate::ir::*; + +use crate::markers; + +use crate::source_graph::{Enum, Struct}; + +use crate::parser::{extract_comments, extract_metadata, type_to_string}; + +pub struct TypeParser<'a> { + src_structs: HashMap, + src_enums: HashMap, + + parsing_or_parsed_struct_names: HashSet, + struct_pool: IrStructPool, + + parsed_enums: HashSet, + enum_pool: IrEnumPool, +} + +impl<'a> TypeParser<'a> { + pub fn new( + src_structs: HashMap, + src_enums: HashMap, + ) -> Self { + TypeParser { + src_structs, + src_enums, + struct_pool: HashMap::new(), + enum_pool: HashMap::new(), + parsing_or_parsed_struct_names: HashSet::new(), + parsed_enums: HashSet::new(), + } + } + + pub fn consume(self) -> (IrStructPool, IrEnumPool) { + (self.struct_pool, self.enum_pool) + } +} + +/// Generic intermediate representation of a type that can appear inside a function signature. +#[derive(Debug)] +pub enum SupportedInnerType { + /// Path types with up to 1 generic type argument on the final segment. All segments before + /// the last segment are ignored. The generic type argument must also be a valid + /// `SupportedInnerType`. + Path(SupportedPathType), + /// Array type + Array(Box, usize), + /// The unit type `()`. + Unit, +} + +impl std::fmt::Display for SupportedInnerType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Path(p) => write!(f, "{}", p), + Self::Array(u, len) => write!(f, "[{}; {}]", u, len), + Self::Unit => write!(f, "()"), + } + } +} + +/// Represents a named type, with an optional path and up to 1 generic type argument. +#[derive(Debug)] +pub struct SupportedPathType { + pub ident: syn::Ident, + pub generic: Option>, +} + +impl std::fmt::Display for SupportedPathType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let ident = self.ident.to_string(); + if let Some(generic) = &self.generic { + write!(f, "{}<{}>", ident, generic) + } else { + write!(f, "{}", ident) + } + } +} + +impl SupportedInnerType { + /// Given a `syn::Type`, returns a simplified representation of the type if it's supported, + /// or `None` otherwise. + pub fn try_from_syn_type(ty: &syn::Type) -> Option { + match ty { + syn::Type::Path(syn::TypePath { path, .. }) => { + let last_segment = path.segments.last().unwrap().clone(); + match last_segment.arguments { + syn::PathArguments::None => Some(SupportedInnerType::Path(SupportedPathType { + ident: last_segment.ident, + generic: None, + })), + syn::PathArguments::AngleBracketed(a) => { + let generic = match a.args.into_iter().next() { + Some(syn::GenericArgument::Type(t)) => { + Some(Box::new(SupportedInnerType::try_from_syn_type(&t)?)) + } + _ => None, + }; + + Some(SupportedInnerType::Path(SupportedPathType { + ident: last_segment.ident, + generic, + })) + } + _ => None, + } + } + syn::Type::Array(syn::TypeArray { elem, len, .. }) => { + let len: usize = match len { + syn::Expr::Lit(lit) => match &lit.lit { + syn::Lit::Int(x) => x.base10_parse().unwrap(), + _ => panic!("Cannot parse array length"), + }, + _ => panic!("Cannot parse array length"), + }; + Some(SupportedInnerType::Array( + Box::new(SupportedInnerType::try_from_syn_type(elem)?), + len, + )) + } + syn::Type::Tuple(syn::TypeTuple { elems, .. }) if elems.is_empty() => { + Some(SupportedInnerType::Unit) + } + _ => None, + } + } +} + +impl<'a> TypeParser<'a> { + pub fn parse_type(&mut self, ty: &syn::Type) -> IrType { + let supported_type = SupportedInnerType::try_from_syn_type(ty) + .unwrap_or_else(|| panic!("Unsupported type `{}`", type_to_string(ty))); + + self.convert_to_ir_type(supported_type) + .unwrap_or_else(|| panic!("parse_type failed for ty={}", type_to_string(ty))) + } + + /// Converts an inner type into an `IrType` if possible. + pub fn convert_to_ir_type(&mut self, ty: SupportedInnerType) -> Option { + match ty { + SupportedInnerType::Path(p) => self.convert_path_to_ir_type(p), + SupportedInnerType::Array(p, len) => self.convert_array_to_ir_type(*p, len), + SupportedInnerType::Unit => Some(IrType::Primitive(IrTypePrimitive::Unit)), + } + } + + /// Converts an array type into an `IrType` if possible. + pub fn convert_array_to_ir_type( + &mut self, + generic: SupportedInnerType, + _len: usize, + ) -> Option { + self.convert_to_ir_type(generic).map(|inner| match inner { + Primitive(primitive) => PrimitiveList(IrTypePrimitiveList { primitive }), + others => GeneralList(IrTypeGeneralList { + inner: Box::new(others), + }), + }) + } + + /// Converts a path type into an `IrType` if possible. + pub fn convert_path_to_ir_type(&mut self, p: SupportedPathType) -> Option { + let p_as_str = format!("{}", &p); + let ident_string = &p.ident.to_string(); + if let Some(generic) = p.generic { + match ident_string.as_str() { + "SyncReturn" => { + // Special-case SyncReturn>. SyncReturn for any other type is not + // supported. + match *generic { + SupportedInnerType::Path(SupportedPathType { + ident, + generic: Some(generic), + }) if ident == "Vec" => match *generic { + SupportedInnerType::Path(SupportedPathType { + ident, + generic: None, + }) if ident == "u8" => { + Some(IrType::Delegate(IrTypeDelegate::SyncReturnVecU8)) + } + _ => None, + }, + _ => None, + } + } + "Vec" => { + // Special-case Vec as StringList + if matches!(*generic, SupportedInnerType::Path(SupportedPathType { ref ident, .. }) if ident == "String") + { + Some(IrType::Delegate(IrTypeDelegate::StringList)) + } else { + self.convert_to_ir_type(*generic).map(|inner| match inner { + Primitive(primitive) => { + PrimitiveList(IrTypePrimitiveList { primitive }) + } + others => GeneralList(IrTypeGeneralList { + inner: Box::new(others), + }), + }) + } + } + "ZeroCopyBuffer" => { + let inner = self.convert_to_ir_type(*generic); + if let Some(IrType::PrimitiveList(IrTypePrimitiveList { primitive })) = inner { + Some(IrType::Delegate( + IrTypeDelegate::ZeroCopyBufferVecPrimitive(primitive), + )) + } else { + None + } + } + "Box" => self.convert_to_ir_type(*generic).map(|inner| { + Boxed(IrTypeBoxed { + exist_in_real_api: true, + inner: Box::new(inner), + }) + }), + "Option" => { + // Disallow nested Option + if matches!(*generic, SupportedInnerType::Path(SupportedPathType { ref ident, .. }) if ident == "Option") + { + panic!( + "Nested optionals without indirection are not supported. (Option>)", + p_as_str + ); + } + self.convert_to_ir_type(*generic).map(|inner| match inner { + Primitive(prim) => IrType::Optional(IrTypeOptional::new_prim(prim)), + st @ StructRef(_) => { + IrType::Optional(IrTypeOptional::new_ptr(Boxed(IrTypeBoxed { + inner: Box::new(st), + exist_in_real_api: false, + }))) + } + other => IrType::Optional(IrTypeOptional::new_ptr(other)), + }) + } + _ => None, + } + } else { + IrTypePrimitive::try_from_rust_str(ident_string) + .map(Primitive) + .or_else(|| { + if ident_string == "String" { + Some(IrType::Delegate(IrTypeDelegate::String)) + } else if self.src_structs.contains_key(ident_string) { + if !self.parsing_or_parsed_struct_names.contains(ident_string) { + self.parsing_or_parsed_struct_names + .insert(ident_string.to_owned()); + let api_struct = self.parse_struct_core(&p.ident); + self.struct_pool.insert(ident_string.to_owned(), api_struct); + } + + Some(StructRef(IrTypeStructRef { + name: ident_string.to_owned(), + freezed: self + .struct_pool + .get(ident_string) + .map(IrStruct::using_freezed) + .unwrap_or(false), + })) + } else if self.src_enums.contains_key(ident_string) { + if self.parsed_enums.insert(ident_string.to_owned()) { + let enu = self.parse_enum_core(&p.ident); + self.enum_pool.insert(ident_string.to_owned(), enu); + } + + Some(EnumRef(IrTypeEnumRef { + name: ident_string.to_owned(), + is_struct: self + .enum_pool + .get(ident_string) + .map(IrEnum::is_struct) + .unwrap_or(true), + })) + } else { + None + } + }) + } + } +} + +impl<'a> TypeParser<'a> { + fn parse_enum_core(&mut self, ident: &syn::Ident) -> IrEnum { + let src_enum = self.src_enums[&ident.to_string()]; + let name = src_enum.ident.to_string(); + let wrapper_name = if src_enum.mirror { + Some(format!("mirror_{}", name)) + } else { + None + }; + let path = src_enum.path.clone(); + let comments = extract_comments(&src_enum.src.attrs); + let variants = src_enum + .src + .variants + .iter() + .map(|variant| IrVariant { + name: IrIdent::new(variant.ident.to_string()), + comments: extract_comments(&variant.attrs), + kind: match variant.fields.iter().next() { + None => IrVariantKind::Value, + Some(Field { + attrs, + ident: field_ident, + .. + }) => { + let variant_ident = variant.ident.to_string(); + IrVariantKind::Struct(IrStruct { + name: variant_ident, + wrapper_name: None, + path: None, + is_fields_named: field_ident.is_some(), + dart_metadata: extract_metadata(attrs), + comments: extract_comments(attrs), + fields: variant + .fields + .iter() + .enumerate() + .map(|(idx, field)| IrField { + name: IrIdent::new( + field + .ident + .as_ref() + .map(ToString::to_string) + .unwrap_or_else(|| format!("field{}", idx)), + ), + ty: self.parse_type(&field.ty), + is_final: true, + comments: extract_comments(&field.attrs), + }) + .collect(), + }) + } + }, + }) + .collect(); + IrEnum::new(name, wrapper_name, path, comments, variants) + } + + fn parse_struct_core(&mut self, ident: &syn::Ident) -> IrStruct { + let src_struct = self.src_structs[&ident.to_string()]; + let mut fields = Vec::new(); + + let (is_fields_named, struct_fields) = match &src_struct.src.fields { + Fields::Named(FieldsNamed { named, .. }) => (true, named), + Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => (false, unnamed), + _ => panic!("unsupported type: {:?}", src_struct.src.fields), + }; + + for (idx, field) in struct_fields.iter().enumerate() { + let field_name = field + .ident + .as_ref() + .map_or(format!("field{}", idx), ToString::to_string); + let field_type = self.parse_type(&field.ty); + fields.push(IrField { + name: IrIdent::new(field_name), + ty: field_type, + is_final: !markers::has_non_final(&field.attrs), + comments: extract_comments(&field.attrs), + }); + } + + let name = src_struct.ident.to_string(); + let wrapper_name = if src_struct.mirror { + Some(format!("mirror_{}", name)) + } else { + None + }; + let path = Some(src_struct.path.clone()); + let metadata = extract_metadata(&src_struct.src.attrs); + let comments = extract_comments(&src_struct.src.attrs); + IrStruct { + name, + wrapper_name, + path, + fields, + is_fields_named, + dart_metadata: metadata, + comments, + } + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/source_graph.rs b/libs/flutter_rust_bridge_codegen/src/source_graph.rs new file mode 100644 index 000000000..de9e3cbfe --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/source_graph.rs @@ -0,0 +1,553 @@ +/* + Things this doesn't currently support that it might need to later: + + - Import parsing is unfinished and so is currently disabled + - When import parsing is enabled: + - Import renames (use a::b as c) - these are silently ignored + - Imports that start with two colons (use ::a::b) - these are also silently ignored +*/ + +use std::{collections::HashMap, fmt::Debug, fs, path::PathBuf}; + +use cargo_metadata::MetadataCommand; +use log::{debug, warn}; +use syn::{Attribute, Ident, ItemEnum, ItemStruct, UseTree}; + +use crate::markers; + +/// Represents a crate, including a map of its modules, imports, structs and +/// enums. +#[derive(Debug, Clone)] +pub struct Crate { + pub name: String, + pub manifest_path: PathBuf, + pub root_src_file: PathBuf, + pub root_module: Module, +} + +impl Crate { + pub fn new(manifest_path: &str) -> Self { + let mut cmd = MetadataCommand::new(); + cmd.manifest_path(&manifest_path); + + let metadata = cmd.exec().unwrap(); + + let root_package = metadata.root_package().unwrap(); + let root_src_file = { + let lib_file = root_package + .manifest_path + .parent() + .unwrap() + .join("src/lib.rs"); + let main_file = root_package + .manifest_path + .parent() + .unwrap() + .join("src/main.rs"); + + if lib_file.exists() { + fs::canonicalize(lib_file).unwrap() + } else if main_file.exists() { + fs::canonicalize(main_file).unwrap() + } else { + panic!("No src/lib.rs or src/main.rs found for this Cargo.toml file"); + } + }; + + let source_rust_content = fs::read_to_string(&root_src_file).unwrap(); + let file_ast = syn::parse_file(&source_rust_content).unwrap(); + + let mut result = Crate { + name: root_package.name.clone(), + manifest_path: fs::canonicalize(manifest_path).unwrap(), + root_src_file: root_src_file.clone(), + root_module: Module { + visibility: Visibility::Public, + file_path: root_src_file, + module_path: vec!["crate".to_string()], + source: Some(ModuleSource::File(file_ast)), + scope: None, + }, + }; + + result.resolve(); + + result + } + + /// Create a map of the modules for this crate + pub fn resolve(&mut self) { + self.root_module.resolve(); + } +} + +/// Mirrors syn::Visibility, but can be created without a token +#[derive(Debug, Clone)] +pub enum Visibility { + Public, + Crate, + Restricted, // Not supported + Inherited, // Usually means private +} + +fn syn_vis_to_visibility(vis: &syn::Visibility) -> Visibility { + match vis { + syn::Visibility::Public(_) => Visibility::Public, + syn::Visibility::Crate(_) => Visibility::Crate, + syn::Visibility::Restricted(_) => Visibility::Restricted, + syn::Visibility::Inherited => Visibility::Inherited, + } +} + +#[derive(Debug, Clone)] +pub struct Import { + pub path: Vec, + pub visibility: Visibility, +} + +#[derive(Debug, Clone)] +pub enum ModuleSource { + File(syn::File), + ModuleInFile(Vec), +} + +#[derive(Clone)] +pub struct Struct { + pub ident: Ident, + pub src: ItemStruct, + pub visibility: Visibility, + pub path: Vec, + pub mirror: bool, +} + +impl Debug for Struct { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Struct") + .field("ident", &self.ident) + .field("src", &"omitted") + .field("visibility", &self.visibility) + .field("path", &self.path) + .field("mirror", &self.mirror) + .finish() + } +} + +#[derive(Clone)] +pub struct Enum { + pub ident: Ident, + pub src: ItemEnum, + pub visibility: Visibility, + pub path: Vec, + pub mirror: bool, +} + +impl Debug for Enum { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Enum") + .field("ident", &self.ident) + .field("src", &"omitted") + .field("visibility", &self.visibility) + .field("path", &self.path) + .field("mirror", &self.mirror) + .finish() + } +} + +#[derive(Debug, Clone)] +pub struct ModuleScope { + pub modules: Vec, + pub enums: Vec, + pub structs: Vec, + pub imports: Vec, +} + +#[derive(Clone)] +pub struct Module { + pub visibility: Visibility, + pub file_path: PathBuf, + pub module_path: Vec, + pub source: Option, + pub scope: Option, +} + +impl Debug for Module { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Module") + .field("visibility", &self.visibility) + .field("module_path", &self.module_path) + .field("file_path", &self.file_path) + .field("source", &"omitted") + .field("scope", &self.scope) + .finish() + } +} + +/// Get a struct or enum ident, possibly remapped by a mirror marker +fn get_ident(ident: &Ident, attrs: &[Attribute]) -> (Ident, bool) { + markers::extract_mirror_marker(attrs) + .and_then(|path| path.get_ident().map(|ident| (ident.clone(), true))) + .unwrap_or_else(|| (ident.clone(), false)) +} + +impl Module { + pub fn resolve(&mut self) { + self.resolve_modules(); + // self.resolve_imports(); + } + + /// Maps out modules, structs and enums within the scope of this module + fn resolve_modules(&mut self) { + let mut scope_modules = Vec::new(); + let mut scope_structs = Vec::new(); + let mut scope_enums = Vec::new(); + + let items = match self.source.as_ref().unwrap() { + ModuleSource::File(file) => &file.items, + ModuleSource::ModuleInFile(items) => items, + }; + + for item in items.iter() { + match item { + syn::Item::Struct(item_struct) => { + let (ident, mirror) = get_ident(&item_struct.ident, &item_struct.attrs); + let ident_str = ident.to_string(); + scope_structs.push(Struct { + ident, + src: item_struct.clone(), + visibility: syn_vis_to_visibility(&item_struct.vis), + path: { + let mut path = self.module_path.clone(); + path.push(ident_str); + path + }, + mirror, + }); + } + syn::Item::Enum(item_enum) => { + let (ident, mirror) = get_ident(&item_enum.ident, &item_enum.attrs); + let ident_str = ident.to_string(); + scope_enums.push(Enum { + ident, + src: item_enum.clone(), + visibility: syn_vis_to_visibility(&item_enum.vis), + path: { + let mut path = self.module_path.clone(); + path.push(ident_str); + path + }, + mirror, + }); + } + syn::Item::Mod(item_mod) => { + let ident = item_mod.ident.clone(); + + let mut module_path = self.module_path.clone(); + module_path.push(ident.to_string()); + + scope_modules.push(match &item_mod.content { + Some(content) => { + let mut child_module = Module { + visibility: syn_vis_to_visibility(&item_mod.vis), + file_path: self.file_path.clone(), + module_path, + source: Some(ModuleSource::ModuleInFile(content.1.clone())), + scope: None, + }; + + child_module.resolve(); + + child_module + } + None => { + let folder_path = + self.file_path.parent().unwrap().join(ident.to_string()); + let folder_exists = folder_path.exists(); + + let file_path = if folder_exists { + folder_path.join("mod.rs") + } else { + self.file_path + .parent() + .unwrap() + .join(ident.to_string() + ".rs") + }; + + let file_exists = file_path.exists(); + + if !file_exists { + warn!( + "Skipping unresolvable module {} (tried {})", + &ident, + file_path.to_string_lossy() + ); + continue; + } + + let source = if file_exists { + let source_rust_content = fs::read_to_string(&file_path).unwrap(); + debug!("Trying to parse {:?}", file_path); + Some(ModuleSource::File( + syn::parse_file(&source_rust_content).unwrap(), + )) + } else { + None + }; + + let mut child_module = Module { + visibility: syn_vis_to_visibility(&item_mod.vis), + file_path, + module_path, + source, + scope: None, + }; + + if file_exists { + child_module.resolve(); + } + + child_module + } + }); + } + _ => {} + } + } + + self.scope = Some(ModuleScope { + modules: scope_modules, + enums: scope_enums, + structs: scope_structs, + imports: vec![], // Will be filled in by resolve_imports() + }); + } + + #[allow(dead_code)] + fn resolve_imports(&mut self) { + let imports = &mut self.scope.as_mut().unwrap().imports; + + let items = match self.source.as_ref().unwrap() { + ModuleSource::File(file) => &file.items, + ModuleSource::ModuleInFile(items) => items, + }; + + for item in items.iter() { + if let syn::Item::Use(item_use) = item { + let flattened_imports = flatten_use_tree(&item_use.tree); + + for import in flattened_imports { + imports.push(Import { + path: import, + visibility: syn_vis_to_visibility(&item_use.vis), + }); + } + } + } + } + + pub fn collect_structs<'a>(&'a self, container: &mut HashMap) { + let scope = self.scope.as_ref().unwrap(); + for scope_struct in &scope.structs { + container.insert(scope_struct.ident.to_string(), scope_struct); + } + for scope_module in &scope.modules { + scope_module.collect_structs(container); + } + } + + pub fn collect_structs_to_vec(&self) -> HashMap { + let mut ans = HashMap::new(); + self.collect_structs(&mut ans); + ans + } + + pub fn collect_enums<'a>(&'a self, container: &mut HashMap) { + let scope = self.scope.as_ref().unwrap(); + for scope_enum in &scope.enums { + container.insert(scope_enum.ident.to_string(), scope_enum); + } + for scope_module in &scope.modules { + scope_module.collect_enums(container); + } + } + + pub fn collect_enums_to_vec(&self) -> HashMap { + let mut ans = HashMap::new(); + self.collect_enums(&mut ans); + ans + } +} + +fn flatten_use_tree_rename_abort_warning(use_tree: &UseTree) { + debug!("WARNING: flatten_use_tree() found an import rename (use a::b as c). flatten_use_tree() will now abort."); + debug!("WARNING: This happened while parsing {:?}", use_tree); + debug!("WARNING: This use statement will be ignored."); +} + +/// Takes a use tree and returns a flat list of use paths (list of string tokens) +/// +/// Example: +/// use a::{b::c, d::e}; +/// becomes +/// [ +/// ["a", "b", "c"], +/// ["a", "d", "e"] +/// ] +/// +/// Warning: As of writing, import renames (import a::b as c) are silently +/// ignored. +fn flatten_use_tree(use_tree: &UseTree) -> Vec> { + // Vec<(path, is_complete)> + let mut result = vec![(vec![], false)]; + + let mut counter: usize = 0; + + loop { + counter += 1; + + if counter > 10000 { + panic!("flatten_use_tree: Use statement complexity limit exceeded. This is probably a bug."); + } + + // If all paths are complete, break from the loop + if result.iter().all(|result_item| result_item.1) { + break; + } + + let mut items_to_push = Vec::new(); + + for path_tuple in &mut result { + let path = &mut path_tuple.0; + let is_complete = &mut path_tuple.1; + + if *is_complete { + continue; + } + + let mut tree_cursor = use_tree; + + for path_item in path.iter() { + match tree_cursor { + UseTree::Path(use_path) => { + let ident = use_path.ident.to_string(); + if *path_item != ident { + panic!("This ident did not match the one we already collected. This is a bug."); + } + tree_cursor = use_path.tree.as_ref(); + } + UseTree::Group(use_group) => { + let mut moved_tree_cursor = false; + + for tree in use_group.items.iter() { + match tree { + UseTree::Path(use_path) => { + if path_item == &use_path.ident.to_string() { + tree_cursor = use_path.tree.as_ref(); + moved_tree_cursor = true; + break; + } + } + // Since we're not matching UseTree::Group here, a::b::{{c}, {d}} might + // break. But also why would anybody do that + _ => unreachable!(), + } + } + + if !moved_tree_cursor { + unreachable!(); + } + } + _ => unreachable!(), + } + } + + match tree_cursor { + UseTree::Name(use_name) => { + path.push(use_name.ident.to_string()); + *is_complete = true; + } + UseTree::Path(use_path) => { + path.push(use_path.ident.to_string()); + } + UseTree::Glob(_) => { + path.push("*".to_string()); + *is_complete = true; + } + UseTree::Group(use_group) => { + // We'll modify the first one in-place, and make clones for + // all subsequent ones + let mut first: bool = true; + // Capture the path in this state, since we're about to + // modify it + let path_copy = path.clone(); + for tree in use_group.items.iter() { + let mut new_path_tuple = if first { + None + } else { + let new_path = path_copy.clone(); + items_to_push.push((new_path, false)); + Some(items_to_push.iter_mut().last().unwrap()) + }; + + match tree { + UseTree::Path(use_path) => { + let ident = use_path.ident.to_string(); + + if first { + path.push(ident); + } else { + new_path_tuple.unwrap().0.push(ident); + } + } + UseTree::Name(use_name) => { + let ident = use_name.ident.to_string(); + + if first { + path.push(ident); + *is_complete = true; + } else { + let path_tuple = new_path_tuple.as_mut().unwrap(); + path_tuple.0.push(ident); + path_tuple.1 = true; + } + } + UseTree::Glob(_) => { + if first { + path.push("*".to_string()); + *is_complete = true; + } else { + let path_tuple = new_path_tuple.as_mut().unwrap(); + path_tuple.0.push("*".to_string()); + path_tuple.1 = true; + } + } + UseTree::Group(_) => { + panic!( + "Directly-nested use groups ({}) are not supported by flutter_rust_bridge. Use {} instead.", + "use a::{{b}, c}", + "a::{b, c}" + ); + } + // UseTree::Group(_) => panic!(), + UseTree::Rename(_) => { + flatten_use_tree_rename_abort_warning(use_tree); + return vec![]; + } + } + + first = false; + } + } + UseTree::Rename(_) => { + flatten_use_tree_rename_abort_warning(use_tree); + return vec![]; + } + } + } + + for item in items_to_push { + result.push(item); + } + } + + result.into_iter().map(|val| val.0).collect() +} diff --git a/libs/flutter_rust_bridge_codegen/src/transformer.rs b/libs/flutter_rust_bridge_codegen/src/transformer.rs new file mode 100644 index 000000000..3ca79620e --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/transformer.rs @@ -0,0 +1,46 @@ +use log::debug; + +use crate::ir::IrType::*; +use crate::ir::*; + +pub fn transform(src: IrFile) -> IrFile { + let dst_funcs = src + .funcs + .into_iter() + .map(|src_func| IrFunc { + inputs: src_func + .inputs + .into_iter() + .map(transform_func_input_add_boxed) + .collect(), + ..src_func + }) + .collect(); + + IrFile { + funcs: dst_funcs, + ..src + } +} + +fn transform_func_input_add_boxed(input: IrField) -> IrField { + match &input.ty { + StructRef(_) + | EnumRef(IrTypeEnumRef { + is_struct: true, .. + }) => { + debug!( + "transform_func_input_add_boxed wrap Boxed to field={:?}", + input + ); + IrField { + ty: Boxed(IrTypeBoxed { + exist_in_real_api: false, // <-- + inner: Box::new(input.ty.clone()), + }), + ..input + } + } + _ => input, + } +} diff --git a/libs/flutter_rust_bridge_codegen/src/utils.rs b/libs/flutter_rust_bridge_codegen/src/utils.rs new file mode 100644 index 000000000..fa822b808 --- /dev/null +++ b/libs/flutter_rust_bridge_codegen/src/utils.rs @@ -0,0 +1,26 @@ +use std::fs; +use std::path::Path; + +pub fn mod_from_rust_path(code_path: &str, crate_path: &str) -> String { + Path::new(code_path) + .strip_prefix(Path::new(crate_path).join("src")) + .unwrap() + .with_extension("") + .into_os_string() + .into_string() + .unwrap() + .replace('/', "::") +} + +pub fn with_changed_file anyhow::Result<()>>( + path: &str, + append_content: &str, + f: F, +) -> anyhow::Result<()> { + let content_original = fs::read_to_string(&path)?; + fs::write(&path, content_original.clone() + append_content)?; + + f()?; + + Ok(fs::write(&path, content_original)?) +}