Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API Proposal]: Generate d.ts file from JSExport attributes #74233

Open
yamachu opened this issue Aug 19, 2022 · 20 comments
Open

[API Proposal]: Generate d.ts file from JSExport attributes #74233

yamachu opened this issue Aug 19, 2022 · 20 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation arch-wasm WebAssembly architecture area-System.Runtime.InteropServices.JavaScript os-browser Browser variant of arch-wasm
Milestone

Comments

@yamachu
Copy link
Contributor

yamachu commented Aug 19, 2022

Background and motivation

JSExport Attribute can make .NET static methods accessible to JavaScript side.

However, access to such methods requires the correct namespace, class name, and method name, and may result in access to invalid objects.

By outputting d.ts when generating JavaScript Interop source, it is possible to type exported objects, which will improve the development experience.

API Proposal

dotnet.d.ts

declare type APIType = {
    runMain: (mainAssemblyName: string, args: string[]) => Promise<number>;
    runMainAndExit: (mainAssemblyName: string, args: string[]) => Promise<number>;
    setEnvironmentVariable: (name: string, value: string) => void;
-   getAssemblyExports(assemblyName: string): Promise<any>;
+   getAssemblyExports<T = any>(assemblyName: string): Promise<T>;

dotnet.g.d.ts

import "@microsoft/dotnet-runtime";

declare module "@microsoft/dotnet-runtime" {
    interface AssemblyExports {
        MyClass: {
            Greeting: () => void;
        }
    }
}

API Usage

main.mts

import { dotnet, AssemblyExports } from "@microsoft/dotnet-runtime";

const { getAssemblyExports } = await dotnet
    .withDiagnosticTracing(false)
    .create();

const config = getConfig();
const exports = await getAssemblyExports<AssemblyExports>(config.mainAssemblyName);
const text = exports.MyClass.Greeting();
console.log(text);

wasmconsole.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <WasmMainJSPath>main.mjs</WasmMainJSPath>
    <OutputType>Exe</OutputType>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <JSInteropTypeDefinitionOutputDir>./types</JSInteropTypeDefinitionOutputDir>
  </PropertyGroup>

  <ItemGroup>
    <CompilerVisibleProperty Include="JSInteropTypeDefinitionOutputDir" />
  </ItemGroup>
</Project>

Alternative Designs

PoC: https://github.com/yamachu/NetWebAssemblyTSTypeGenerator

Risks

  • It may not work well in JSDoc
  • It will not work if the package.json does not contain a runtime dependency
@yamachu yamachu added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Aug 19, 2022
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Aug 19, 2022
@maraf maraf added arch-wasm WebAssembly architecture area-System.Runtime.InteropServices.JavaScript and removed untriaged New issue has not been triaged by the area owner labels Aug 19, 2022
@ghost
Copy link

ghost commented Aug 19, 2022

Tagging subscribers to 'arch-wasm': @lewing
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

JSExport Attribute can make .NET static methods accessible to JavaScript side.

However, access to such methods requires the correct namespace, class name, and method name, and may result in access to invalid objects.

By outputting d.ts when generating JavaScript Interop source, it is possible to type exported objects, which will improve the development experience.

API Proposal

dotnet.d.ts

declare type APIType = {
    runMain: (mainAssemblyName: string, args: string[]) => Promise<number>;
    runMainAndExit: (mainAssemblyName: string, args: string[]) => Promise<number>;
    setEnvironmentVariable: (name: string, value: string) => void;
-   getAssemblyExports(assemblyName: string): Promise<any>;
+   getAssemblyExports<T = any>(assemblyName: string): Promise<T>;

dotnet.g.d.ts

import "@microsoft/dotnet-runtime";

declare module "@microsoft/dotnet-runtime" {
    interface AssemblyExports {
        MyClass: {
            Greeting: () => void;
        }
    }
}

API Usage

main.mts

import { dotnet, AssemblyExports } from "@microsoft/dotnet-runtime";

const { getAssemblyExports } = await dotnet
    .withDiagnosticTracing(false)
    .create();

const config = getConfig();
const exports = await getAssemblyExports<AssemblyExports>(config.mainAssemblyName);
const text = exports.MyClass.Greeting();
console.log(text);

wasmconsole.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
    <WasmMainJSPath>main.mjs</WasmMainJSPath>
    <OutputType>Exe</OutputType>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <JSInteropTypeDefinitionOutputDir>./types</JSInteropTypeDefinitionOutputDir>
  </PropertyGroup>

  <ItemGroup>
    <CompilerVisibleProperty Include="JSInteropTypeDefinitionOutputDir" />
  </ItemGroup>
</Project>

Alternative Designs

PoC: https://github.com/yamachu/NetWebAssemblyTSTypeGenerator

Risks

  • It may not work well in JSDoc
  • It will not work if the package.json does not contain a runtime dependency
Author: yamachu
Assignees: -
Labels:

api-suggestion, arch-wasm, area-System.Runtime.InteropServices.JavaScript

Milestone: -

@jkoritzinsky
Copy link
Member

I/O in a generator is unsupported, so this needs to wait for dotnet/roslyn#57608 to be doable in a supported fashion.

@pavelsavara
Copy link
Member

Does this usage work as is ?

const exports:AssemblyExports = await getAssemblyExports(config.mainAssemblyName);

I'm not sure that the TypeScript definitions generator need to be part of the runtime.
I think that 3rd party generator is a good choice.

@radical radical added this to the 8.0.0 milestone Aug 29, 2022
@Thaina
Copy link

Thaina commented Oct 29, 2022

On the flipside. Are there any way or plan to generate C# wrapper over javascript package from its d.ts ?

So we could write C# wasm with any existing npm package that provide d.ts

@pavelsavara
Copy link
Member

The JS interop only supports static functions and limited set of marshaled argument types.
It would not be able to automatically wrap any sufficiently complex .d.ts

@Thaina
Copy link

Thaina commented Nov 24, 2022

@pavelsavara Isn't all the type of js can be reference in C# as JSObject ?

And then we make a class as a wrapper of JSObject and make a property for each of field they have. Given that we also able to convert Promise to Task I don't see anything we cannot convert

Could you please give me an example of what impossible to generate right now?

@pavelsavara
Copy link
Member

Yes, you can generate wrapper class around the JSObject. Let's say you want to have WebSocket instance and you would generate C# class for it. Which part of the interop pipeline would create the instance of WebSocket C# proxy ?

Now let's say that we have TypeScript definition of WebSocket.send(). You can see that there are multiple types of the data parameter. This is not easy to do in strongly typed language. It's doable when there is somebody who would make design decisions about the type mapping. But I believe it's not practical to do it in automated generator. Especially when there are more than one dynamically typed parameter, it leads to exponential number of strongly typed method signatures.

Even if somebody would really want to try to do it, I still think that it's not something we need to do in the runtime repo.
It could be 3rd party generator, nuget. Ideally FOSS.

@Thaina
Copy link

Thaina commented Nov 24, 2022

@pavelsavara I kind of thinking that this feature should be native to wasm and/or blazor build system. And it was not ideal that we could not decouple wasm and blazor from runtime repo

It would be very convenient for dotnet wasm compiler to able to import existing package in npm into C# so we could utilize every critical 3rd party package in the web world that will only written as npm package

@Thaina
Copy link

Thaina commented Nov 24, 2022

Also while it would impractical for general type in JavaScript. I think we could scope the possibility into only class that have d.ts file or jsdoc. Then just handwaving any ambiguous type as JSObject

@pavelsavara pavelsavara modified the milestones: 8.0.0, Future Nov 24, 2022
@Thaina
Copy link

Thaina commented Nov 25, 2022

@pavelsavara Should I made separate issue about codegen for C# side or would you consider this feature as the same interoperability system between cs and d.ts ?

@pavelsavara
Copy link
Member

Let's keep the discussion in single place for now.

@pavelsavara
Copy link
Member

Perhaps making JSObject to implement dynamic would solve some of the pains.
It will be slow, with ugly corner cases, but it will lower the barrier.
#78853

@Thaina
Copy link

Thaina commented Nov 26, 2022

It definitely decrease barrier but being dynamic still could not give us intelligence

@pavelsavara
Copy link
Member

I/O in a generator is unsupported, so this needs to wait for dotnet/roslyn#57608 to be doable in a supported fashion.

Is there strong reason why we could not go with the hack described at the top of dotnet/roslyn#57608 until they fix it ? That is for generating .d.ts

@pavelsavara
Copy link
Member

On topic of generating C# from .d.ts, I assume we are not going to implement TypeScript parser in C# in order to be able to plug it into the Roslyn process, right ? That said, I see one benefit of triggering the C# code gen in Roslyn via attribute. The ability to discover it in the code. What are the other benefits of Roslyn generator based on external metadata ?

Alternatively I could imagine pre-Build MSBuild target which would run generator based on tsc and nodejs. When the wasm-tools workload is installed, we have NodeJS and adding typescript and generator package should be easy.
The generator could be written on top of tcs's AST.

Any other ideas how this could be implemented ?

@Thaina
Copy link

Thaina commented Nov 29, 2022

I expect that we could just run TS language server in the project, make it like TS/JS normal project. Then communicate with it by some means, the generator will act like another IDE to glean the shallow namespace and class we can import from typescript

Then when we using the namespace, the generator then generate the C# wrapper for each type we can access

But that might be later. For the start we might just use tsc and generate everything in the package we import

@pavelsavara
Copy link
Member

I expect that we could just run TS language server in the project, make it like TS/JS normal project. Then communicate with it by some means, the generator will act like another IDE to glean the shallow namespace and class we can import from typescript

"normal js project" could be NextJS, ReactApp, Angular, webpack, just plain TS and anything in between.
Besides it's is not really friends with MSBuild or Visual Studio. We need very narrow scope of tooling effort to make this fly.

For the start we might just use tsc and generate everything in the package we import

Are you planning to start working on it @Thaina ? What kind of support do you need ?

@Thaina
Copy link

Thaina commented Nov 29, 2022

I expect that we could just run TS language server in the project, make it like TS/JS normal project. Then communicate with it by some means, the generator will act like another IDE to glean the shallow namespace and class we can import from typescript

"normal js project" could be NextJS, ReactApp, Angular, webpack, just plain TS and anything in between. Besides it's is not really friends with MSBuild or Visual Studio. We need very narrow scope of tooling effort to make this fly.

If possible I think in the end this system would be general ts->cs generator that don't really care about framework. The use case is just that, it let us access class, property, and function available in typescript definition, and the framework specific feature might be skipped if it not really in the possible pattern of C#

But for the start I think we might focus on pure wasm project that just written in raw html and include javascript, the one that generated from dotnet new wasm command. And another main focus might be Blazor

IMO the one thing that would make this fly would be that, this system let us install npm package, then C# will be able to use it. NPM packages would be most likely framework agnostic. My priority would be that, bring a crucial 3rd party library like firebase into C# wasm so we can write C# webapp that connect to firebase

For the start we might just use tsc and generate everything in the package we import

Are you planning to start working on it @Thaina ? What kind of support do you need ?

I have start investigate the language server's client capability, I try to find the easiest way to have C# communicate with TS. But for now I want solid plan and clear goal of this feature first, so I don't waste my time race against what other would do. And I would like to wait for complete wrapper of JSObject first, because if I would write generator I would like to write the wrapper class over that class, which is far more easy and have clearer pattern than writing direct wrapper with current pattern

@jkoritzinsky
Copy link
Member

I/O in a generator is unsupported, so this needs to wait for dotnet/roslyn#57608 to be doable in a supported fashion.

Is there strong reason why we could not go with the hack described at the top of dotnet/roslyn#57608 until they fix it ? That is for generating .d.ts

@pavelsavara The hack can fail at any time and might not regenerate the file when you expect. I personally see the workaround for use in a "we need this for a high-priority business need", not a nice-to-have feature, especially since if it behaves weirdly, we'll get bugs about the behavior filed and we won't be able to fix them as we're actively doing something the compiler team has told us not to do.

@pavelsavara pavelsavara added the os-browser Browser variant of arch-wasm label Nov 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation arch-wasm WebAssembly architecture area-System.Runtime.InteropServices.JavaScript os-browser Browser variant of arch-wasm
Projects
None yet
Development

No branches or pull requests

6 participants