feat(compiler): add dart gRPC codegen#3723
Conversation
Wire DartServiceGeneratorMixin into DartGenerator via MRO matching JavaGenerator and PythonGenerator. The mixin currently exposes only the streaming-rejection check, which raises ValueError listing every offending method when an FDL service contains stream RPCs. File emission and Client/ServiceBase generation land in follow-up commits.
Add generate_grpc_module to DartServiceGeneratorMixin: emits a <stem>_grpc.dart companion next to the messages file with Apache header, dart:async/dart:typed_data/package:grpc imports, a sibling import of the messages module as _models, and the two top-level codec helpers (_serialize<T> and _deserialize<T>) that route through the existing ForyRegistration.getFory() static. _deserialize includes the is-Uint8List guard for zero-copy on the common HTTP/2 path.
Emit per-service Client with ClientMethod constants and $createUnaryCall-backed unary methods. Constructor uses super-parameter syntax confirmed by grpc 4.3.1's in-tree TestClient(super.channel). Method names are normalized through safe_identifier(to_camel_case(method.name)).
Emit <Service>ServiceBase: registers each unary RPC via $addMethod in the constructor and exposes an abstract handler per method. Uses a _Pre shim that awaits the request Future before dispatching, since grpc-dart's ServiceMethod always hands the handler a Future<Q>
Add check_dart_grpc_service_collisions to the mixin. For each local service, compute its emitted Client and ServiceBase class names and check against the set of top-level names DartGenerator would already emit for messages/enums/unions/registration. Also detect service<->service collisions when two services in the same schema would emit the same Client/ServiceBase name. Raises ValueError naming the offending class; the CLI's existing ValueError wrap at cli.py:686-690 prints it and exits non-zero.
Add check_dart_grpc_method_collisions. For each service, normalize every method name through safe_identifier(to_camel_case(name)) and raise ValueError if two distinct RpcMethod names collapse to the same Dart identifier (e.g. SayHello and say_hello both produce sayHello).
Mirror Python's test_proto_and_fbs_grpc_service_codegen: one test exercises a .proto-sourced service through ProtoTranslator and a .fbs-sourced service through FbsTranslator, then asserts both end up in the Dart gRPC emitter with the right method paths and Client classes. Confirms no Dart-specific translator wiring was needed.
Register DartGenerator in GENERATOR_CLASSES and add it to the gRPC-supported skip in the unsupported-generators test.
Emit _grpc.dart under the same package directory as the messages file. Add opt-in dart analyze gate covering the emitted file.
dart format reflowed the ClientMethod constant and $addMethod call; align emitter strings with format output and add a dart format gate to the smoke test.
|
Could you add java-dart grpc integration tests? You can take #3692 as reference |
# Conflicts: # compiler/fory_compiler/tests/test_service_codegen.py
Generate server-stream, client-stream, and bidi methods alongside unary, and serialize with ref tracking so payloads interop with the Java and Python peers.
Add a Dart interop peer (integration_tests/grpc_tests/dart) mirroring the Python peer's FDL transforms with client and server subcommands, driven both directions by DartGrpcInteropTest over the shared GrpcTestBase via new dartCommand/runDart helpers. Generate Dart stubs in generate_grpc.py, build the peer (dart pub get + build_runner) in run_tests.sh, and add a grpc_java_dart_tests CI job.
|
Thanks for the review, I have add all the remaining rpc methods with the integration tests |
|
Please take #3761 as reference to add comprehensive docs |
|
Thanks, I have updated the docs! |
| "no separate registration helper needed." | ||
| ) | ||
| lines.append("") | ||
| fory = f"_models.{self.module_type_name()}.getFory()" |
There was a problem hiding this comment.
This makes the generated gRPC path depend on callers having already run <Module>ForyModule.install(Fory()); otherwise getFory() throws before the first RPC. Generated gRPC services should not require manual generated-type registration, and the other language companions hide this behind a module-owned ready runtime. Please make the Dart companion construct or otherwise obtain a ready Fory instance for generated gRPC, keeping custom runtime injection optional rather than required.
| lines.append(f"class {service.name}Client extends Client {{") | ||
| for method in service.methods: | ||
| method_const = f"_${self.dart_grpc_method_name(method)}" | ||
| req_t = f"_models.{method.request_type.name}" |
There was a problem hiding this comment.
This raw AST name only works for top-level local message types. For an imported payload such as demo.common.SharedReq, the companion emits _models.demo.common.SharedReq; for a nested payload such as Envelope.Request, it emits _models.Envelope.Request, while Dart model code emits/imports flattened or alias-qualified symbols like Envelope_Request. Please resolve RPC payloads through the Dart generator's emitted type-name path (resolve_type/ref_name) and add analyzer coverage for imported and nested RPC payloads.
| | `GreeterServiceBase` in `demo_greeter_grpc.dart` | Base class for server implementations | | ||
| | `GreeterClient` in `demo_greeter_grpc.dart` | Client stub for gRPC calls | | ||
|
|
||
| ## Register Types |
There was a problem hiding this comment.
This section documents a setup step that generated gRPC users should not need. The gRPC companion should own or obtain a ready Fory instance and register generated service payload types automatically, consistent with the other language gRPC generators. Once the generator handles that, please remove this manual registration section and the install(Fory()) calls from the server/client examples below.
|
|
||
| A single-response client method returns `ResponseFuture<R>` (client-streaming | ||
| adapts the streaming call with `.single`); a streaming-response method returns | ||
| `ResponseStream<R>`. On the server, single requests arrive as `Future<Q>` and |
There was a problem hiding this comment.
This describes the internal _Pre shim, not the public service override. The generated abstract methods receive a concrete Q for single-request RPCs and Stream<Q> for client-streaming RPCs. Please adjust this sentence so users do not implement server methods with Future<Q> request parameters.
|
|
||
| | File | Purpose | | ||
| | ------------------------------------------------ | ---------------------------------------------------- | | ||
| | `demo/greeter/demo_greeter.dart` | Fory model types and the schema module | |
There was a problem hiding this comment.
These generated names do not match the current Dart generator for the command above. With source service.fdl and package demo.greeter, _module_file_name_for_schema uses the package leaf and emits demo/greeter/greeter.dart, greeter.fory.dart, greeter_grpc.dart, and GreeterForyModule, not demo_greeter.* / DemoGreeterForyModule. Please either update the docs/examples to match the current output or change the generator if source-stem-owned module names are the intended design.
| python-version: 3.11 | ||
| cache: "pip" | ||
| - name: Set up Dart | ||
| uses: dart-lang/setup-dart@v1 |
There was a problem hiding this comment.
This adds an unpinned third-party action to ASF CI. The existing Dart release workflow pins dart-lang/setup-dart to a commit SHA (e51d8... # v1.7.1), so this job should use a pinned SHA or the same approved setup path instead of @v1 to avoid mutable action resolution and ASF action-policy issues.
| assert "import 'demo_greeter.dart' as _models;" in content | ||
|
|
||
| assert ( | ||
| "_models.DemoGreeterForyModule.getFory().serialize(value, trackRef: true)" |
There was a problem hiding this comment.
These assertions pin the same manual DemoGreeterForyModule.getFory() ownership path that the generated gRPC companion should avoid. Once the generator owns or obtains a ready Fory runtime automatically, this test needs to assert that new generated path rather than preserving the startup-only install(Fory()) dependency.
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| """dart analyze smoke test for emitted Dart gRPC files. |
There was a problem hiding this comment.
This test should not live under compiler pytest. It shells out to dart pub get, dart format, and dart analyze, and it validates the generated Dart gRPC companion rather than compiler-only behavior. The owner should be the Dart gRPC integration test path under integration_tests/grpc_tests/dart (or the Java-driven gRPC integration harness that already installs Dart), so CI exercises it with the Dart toolchain instead of adding a compiler test that skips when Dart is absent. Please move this validation there and remove this compiler-level test file.
|
|
||
| @Test | ||
| public void testJavaServerDartClient() throws Exception { | ||
| Server server = startJavaFdlServer(); |
There was a problem hiding this comment.
This only exercises the FDL service path, but the PR documents and tests Dart gRPC as supporting Fory IDL, protobuf IDL, and FlatBuffers IDL. Building the generated proto/FBS Dart files is not enough for that feature surface; the Dart client/server interop needs to exercise all claimed schema inputs, like the Python/Rust/Kotlin gRPC tests do with exerciseAllSchemas, or the public docs/tests need to narrow the claim. As it stands, Dart gRPC support is only partially implemented and leaves the protobuf/FBS runtime paths unproven.
| - Dart xlang or runtime ownership changes need local Dart package tests plus the Java-driven `DartXlangTest`; package-only smoke tests are not enough. | ||
| - When claiming non-VM Dart support, prove a relevant non-VM compile path such as `dart compile js` against active runtime or example code. | ||
| - Generated Dart gRPC service companions (`<stem>_grpc.dart`) are compiler-owned files that depend on the application-provided `grpc` package, not `dart/packages/fory`. Keep gRPC dependencies out of the Fory Dart runtime package. | ||
| - Dart generated schema modules (`<Stem>ForyModule`) are the source-file owners. Service companions must resolve their `Fory` through that module's `getFory()` and must not introduce package-derived aliases or duplicate serializer registration paths. |
There was a problem hiding this comment.
This guidance encodes the same owner model that the implementation should move away from. If generated Dart gRPC is meant to be complete, the companion should own or obtain a ready Fory runtime for service payloads; putting getFory() as a required rule in .agents will keep future changes preserving the manual-install dependency. Please update or remove this rule along with the generator/docs so the design source does not stay stale.
| } | ||
|
|
||
| Future<void> main() async { | ||
| DemoGreeterForyModule.install(Fory()); |
There was a problem hiding this comment.
These server/client snippets call Fory() but do not import package:fory/fory.dart, so they do not compile as written while the manual install remains. The preferred fix is still to remove the manual install requirement; if any Fory() construction remains in the docs, the examples need complete imports.
| **Fix**: Add `grpc` to your `pubspec.yaml` (and the `build_runner` dev | ||
| dependency), then run `dart pub get`. See [gRPC Support](grpc-support.md). | ||
|
|
||
| ## `getFory()` throws before the first gRPC call |
There was a problem hiding this comment.
This troubleshooting entry turns the current startup failure into documented behavior. A first gRPC call should not fail because the user did not call <Schema>ForyModule.install(fory); generated service code should be ready by construction or through explicit custom injection. Please remove or rewrite this entry when the generator ownership is fixed.
| When a schema contains services and the compiler is run with `--grpc`, Dart | ||
| generation emits one `<module>_grpc.dart` file per schema next to the model | ||
| types. It targets `package:grpc` and serializes each request and response with | ||
| the schema module's `getFory()` instance (for example |
There was a problem hiding this comment.
This compiler-level docs section repeats the manual getFory() runtime ownership model. Once Dart gRPC owns or obtains a ready runtime automatically, this should describe that public workflow instead of telling users service serialization goes through a preinstalled schema module. Otherwise the generated-code docs will remain inconsistent with the service design.
| ) | ||
| service_names.add(emitted) | ||
|
|
||
| def check_dart_grpc_method_collisions(self, services: List[Service]) -> None: |
There was a problem hiding this comment.
This only checks method-vs-method collisions after camelCasing. The generated members also sit on classes extending Client and Service, and every Dart object inherits Object, so valid RPC names like ToString or HashCode generate invalid overrides or conflicting members (toString(...), hashCode(...)). Please reserve those inherited/member surfaces or fail codegen with a clear diagnostic, and add analyzer coverage for those names.
|
@yash-agarwa-l Please merge apache/main first, we updated the docs and ci tests |
# Conflicts: # docs/compiler/compiler-guide.md # docs/compiler/flatbuffers-idl.md # docs/compiler/index.md # docs/compiler/protobuf-idl.md # docs/compiler/schema-idl.md # integration_tests/grpc_tests/run_tests.sh
…lution and reserved-name guard
…est, pin CI dart action, move analyze coverage to integration
Why?
Dart users need gRPC support from FDL/proto/fbs schemas
What does this PR do?
Adds DartServiceGeneratorMixin so foryc --grpc --dart_out=… emits a /_grpc.dart next to the messages file. Includes:
Related issues
Part of #3266
Part of #3279
Closes #3266
AI Contribution Checklist
yes/noyes, I included a completed AI Contribution Checklist in this PR description and the requiredAI Usage Disclosure.yes, my PR description includes the requiredai_reviewsummary and screenshot evidence of the final clean AI review results from both fresh reviewers on the current PR diff or current HEAD after the latest code changes.Does this PR introduce any user-facing change?
Benchmark