Skip to content

Add source generation of proxies - #1223

Merged
Andrew Arnott (AArnott) merged 91 commits into
mainfrom
746-client-proxy-generation-at-compile-time
Jul 23, 2025
Merged

Add source generation of proxies#1223
Andrew Arnott (AArnott) merged 91 commits into
mainfrom
746-client-proxy-generation-at-compile-time

Conversation

@AArnott

@AArnottAndrew Arnott (AArnott) commented Jul 7, 2025

Copy link
Copy Markdown
Member

This cuts down on startup time and gets us a significant step closer to being NativeAOT ready.

🌞 Highlights

Startup performance improves because we don't have to reflect over the RPC interface, Ref.Emit ourselves a type that implements that interface, then JIT compile it. The JIT step was immune to ngen because the type doesn't exist at ngen time. Having a source generated implementation over the RPC interface solves all these problems.

NativeAOT is incapable of Ref.Emit and JIT. It has limited reflection capability. So all these perf improvements also gets us closer to full functionality when run under NativeAOT.

📖 Overview

  • New attributes

    • JsonRpcContractAttribute which users should label their RPC interfaces with.
    • JsonRpcProxyMappingAttribute which is used by the source generator to allow reflection-based discovery of the source generated proxy for a given RPC contract interface.
    • JsonRpcProxyInterfaceGroupAttribute which predefines sets of RPC interfaces for which a proxy should be source generated so they can be implemented together.
    • ExportRpcContractProxiesAttribute is an assembly-targeted attribute that exposes the generated proxies as public types and optionally forbids (by policy) external assemblies from source generating their own proxies. The forbidding policy will be important for assemblies such as RpcContracts, which reserves the right to add interface members, which would become a binary breaking change in a world of source generated proxies otherwise.
  • A new analyzer that reads [JsonRpcContract] attributed interfaces and reports diagnostics when they violate our rules for such interfaces (e.g. no properties, methods must use one of a few return types, etc.) as well as uphold the new requirement that such interfaces must be declared with at least internal visibility so that the source generator can emit a proxy class that implements it.

    This new analyzer also scans [RpcMarshalable] interfaces and upholds its variant of the rules.

  • A source generator that produces proxy classes that implement those [JsonRpcContract] interfaces and the JsonRpcProxyMappingAttribute to allow their discovery at runtime.
    The generated proxy classes are declared as internal (or public) and discovered via the [JsonRpcProxyMappingAttribute] at runtime.
    A project that compiles against the new StreamJsonRpc library may opt-in to C# interceptor behavior that will rewrite (most) calls to JsonRpc.Attach to instead call a custom method that directly instantiates the necessary proxy type. This eliminates all reflection and (more importantly) moves the compiled assembly away from the Attach methods that are attributed as requiring dynamic and unreferenced code which are incompatible with NativeAOT.
    This interceptor is not on by default because it will cause runtime failures if the requested interfaces have no source generated proxy for them, whereas prior behavior would have generated dynamic proxies for any compatible interface.

    • Named arguments support is done with a Dictionary<string, object?> instead of a custom struct. This avoids the reflection required by dynamic proxies.
    • JsonRpc.Attach calls are searched for in the consuming project and when the interface type(s) are statically discoverable, we ensure that proxies for those types are source generated (reducing the need for JsonRpcProxyInterfaceGroupAttribute to only those non-analyzable combinations).
  • A diagnostic suppressor to turn off trim/NativeAOT warnings due to Attach calls that the Interceptor will rewrite anyway.

  • Add and update tests so that the source generator and new proxies are tested in a variety of configurations.

  • New docs for the analyzers.

Out of scope

These are important items, but it will take a bit more work and I'm eager to merge this so we can show some incremental progress.

  • Enable use of source generated proxies for RPC marshaled objects.
  • Stop declaring empty structs for named arguments of methods that take no parameters.

Andrew Arnott (AArnott)and others added 30 commits June 13, 2025 07:34
Comment threadsrc/StreamJsonRpc/JsonRpcProxyOptions.cs Outdated
Comment threadsrc/StreamJsonRpc.Analyzers.CodeFixes/JsonRpcContractCodeFixProvider.cs Outdated
Comment threadsrc/StreamJsonRpc.Analyzers/GeneratorModels/Container.cs
Comment threadsrc/StreamJsonRpc.Analyzers/GeneratorModels/ProxyModel.cs
Comment threadsrc/StreamJsonRpc.Analyzers/SourceWriter.cs
Comment threadsrc/StreamJsonRpc/JsonRpcProxyOptions.cs Outdated
@AArnott
Andrew Arnott (AArnott) enabled auto-merge (squash) July 23, 2025 19:14
@AArnott

Copy link
Copy Markdown
MemberAuthor

@microsoft-github-policy-service rerun

@AArnott
Andrew Arnott (AArnott) merged commit e45c39b into mainJul 23, 2025
6 checks passed
@AArnott
Andrew Arnott (AArnott) deleted the 746-client-proxy-generation-at-compile-time branch July 23, 2025 19:57
Comment on lines +58 to +62
if (!context.SemanticModel.Compilation.IsSymbolAccessibleWithin(context.TargetSymbol, context.SemanticModel.Compilation.Assembly))
{
// Reported by StreamJsonRpc0001
return [];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider doing this check before doing the work to get known symbols.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client proxy generation at compile time

4 participants

@AArnott@eerhardt@333fred@RyanToth3