Skip to content

feat(compiler): add dart gRPC codegen#3723

Open
yash-agarwa-l wants to merge 20 commits into
apache:mainfrom
yash-agarwa-l:grpc-dart
Open

feat(compiler): add dart gRPC codegen#3723
yash-agarwa-l wants to merge 20 commits into
apache:mainfrom
yash-agarwa-l:grpc-dart

Conversation

@yash-agarwa-l

@yash-agarwa-l yash-agarwa-l commented May 31, 2026

Copy link
Copy Markdown
Contributor

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:

  • Client over package:grpc's Client with one method per RPC, covering unary and all three streaming modes (server-stream, client-stream, bidi).
  • abstract ServiceBase over Service with $addMethod registrations and a _Pre shim per method.
  • Top-level _serialize / _deserialize helpers routing through the schema module's getFory(), with reference tracking on so payloads round-trip with the Java/Python peers.
  • Works from FDL, Protobuf, and FlatBuffers IDL service definitions.
  • Class- and method-name collision detection.
  • Opt-in dart analyze + dart format smoke test on the emitted file.
  • A Java↔Dart gRPC interop test exercising all four modes in both directions.

Related issues

Part of #3266
Part of #3279
Closes #3266

AI Contribution Checklist

  • Substantial AI assistance was used in this PR: yes / no
  • If yes, I included a completed AI Contribution Checklist in this PR description and the required AI Usage Disclosure.
  • If yes, my PR description includes the required ai_review summary 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?

  • Does this PR introduce any public API change?
  • Does this PR introduce any binary protocol compatibility change?
  • New _grpc.dart companion when --grpc --dart_out is used on a schema with services.

Benchmark

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.
@chaokunyang

Copy link
Copy Markdown
Collaborator

Could you add java-dart grpc integration tests? You can take #3692 as reference

@chaokunyang chaokunyang self-requested a review June 2, 2026 03:29
# 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.
@yash-agarwa-l yash-agarwa-l changed the title feat(compiler): add dart unary gRPC codegen feat(compiler): add dart gRPC codegen Jun 15, 2026
@yash-agarwa-l

Copy link
Copy Markdown
Contributor Author

Thanks for the review, I have add all the remaining rpc methods with the integration tests

@chaokunyang

Copy link
Copy Markdown
Collaborator

Please take #3761 as reference to add comprehensive docs

@yash-agarwa-l

Copy link
Copy Markdown
Contributor Author

Thanks, I have updated the docs!

"no separate registration helper needed."
)
lines.append("")
fory = f"_models.{self.module_type_name()}.getFory()"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread docs/guide/dart/grpc-support.md Outdated
| `GreeterServiceBase` in `demo_greeter_grpc.dart` | Base class for server implementations |
| `GreeterClient` in `demo_greeter_grpc.dart` | Client stub for gRPC calls |

## Register Types

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread docs/compiler/generated-code.md Outdated

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread docs/guide/dart/grpc-support.md Outdated

| File | Purpose |
| ------------------------------------------------ | ---------------------------------------------------- |
| `demo/greeter/demo_greeter.dart` | Fory model types and the schema module |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread .github/workflows/ci.yml Outdated
python-version: 3.11
cache: "pip"
- name: Set up Dart
uses: dart-lang/setup-dart@v1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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)"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread .agents/languages/dart.md Outdated
- 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread docs/guide/dart/grpc-support.md Outdated
}

Future<void> main() async {
DemoGreeterForyModule.install(Fory());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread docs/guide/dart/troubleshooting.md Outdated
**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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread docs/compiler/generated-code.md Outdated
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@chaokunyang

Copy link
Copy Markdown
Collaborator

@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
…est, pin CI dart action, move analyze coverage to integration
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Compiler] Add Grpc Support

2 participants