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

[wasm][draft] make JSObject dynamic #78853

Closed
wants to merge 2 commits into from

Conversation

pavelsavara
Copy link
Member

@pavelsavara pavelsavara commented Nov 25, 2022

This is exploration of crazy idea, I'm not sure I want to do it.

dynamic globalThis = JSHost.GlobalThis;
Console.WriteLine("Hello from "+ globalThis.location?.href);

Motivation: give developers easier usage of JavaScript object oriented APIs
Downsides: large downloads, GC pressure

@dotnet-issue-labeler
Copy link

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@ghost
Copy link

ghost commented Nov 25, 2022

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

Issue Details

This is exploration of crazy idea, I'm not sure I want to do it.

dynamic globalThis = JSHost.GlobalThis;
Console.WriteLine("Hello from "+ globalThis.location?.href);
Author: pavelsavara
Assignees: pavelsavara
Labels:

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

Milestone: 8.0.0

@jkotas
Copy link
Member

jkotas commented Nov 25, 2022

We have experimented with dynamic for System.Text.Json for similar reasons and rejected the idea after a long discussion: #53195

@Thaina
Copy link

Thaina commented Nov 26, 2022

Then could we at least implement IDictionary and have Invoke function for whenever the JSObject is function?

@kg
Copy link
Member

kg commented Nov 26, 2022

The problems with dynamic in S.T.Json don't seem like they necessarily apply here, since we already are offering lots of equivalent API functionality and it maps well to the actual type model this time. In many cases people want to deserialize JSON as strongly typed objects, perhaps with an explicit schema, so dynamic makes less sense - here in comparison we're dealing with arbitrary JS objects, not JSON blobs, and we're not deserializing them in one operation and are instead interacting with them across a remoting barrier. dynamic might be perfect for that and may also make user code less error prone.

If we don't want anyone to ever use that part of the compiler or BCL again that makes sense, but it's kind of a shame since it might be a perfect fit for this problem.

@jkotas
Copy link
Member

jkotas commented Nov 26, 2022

dynamic in S.T.Json don't seem like they necessarily apply here, since we already are offering lots of equivalent API functionality and it maps well to the actual type model this time.

S.T.Json discussion was about using dynamic for JsonObject. JsonObject is not strongly typed, it is arbitrary mutable DOM. I think it is very similar to this case.

If we don't want anyone to ever use that part of the compiler or BCL again that makes sense, but it's kind of a shame since it might be a perfect fit for this problem.

Microsoft.CSharp runtime has large binary footprint, expensive startup, and issues with trimming. The question to ask whether it is acceptable to pay for all this in typical wasm and Blazor apps.

I agree that it is a shame that dynamic is what it is, and it forces difficult trade-offs.

@pavelsavara
Copy link
Member Author

pavelsavara commented Nov 28, 2022

Microsoft.CSharp runtime has large binary footprint, expensive startup, and issues with trimming. The question to ask whether it is acceptable to pay for all this in typical wasm and Blazor apps.

These are exactly the reasons why I'm not sure this is good idea.

  • It would probably make trimming more difficult.
  • Does Microsoft.CSharp and System.Linq.Expressions itself trim well ? Or is it full for reflection code ?
  • Does all of it AOT well ?

We could add some indirection like JSObject.ToDynamic() extension method.
So that people who don't use it would not have to pay for it.

I agree that it is a shame that dynamic is what it is, and it forces difficult trade-offs.

I make this experiment in in light of #74233 and other similar .
It seems to me that some people want to do interop over OOP features of JavaScript APIs (of browser and libraries).
Some of them want to interact with DOM.
No matter the costs.

The alternative is generators of strongly typed proxies and dealing with type-system mismatch there.

In both cases the OOP interaction would be making lot of small calls over the boundary. And both solutions will generate lot of GC traffic for both VMs. Overall the efficiency is out of window anyway.

The positive aspect of making one of the "solutions" possible is that it would lower the barrier of using JS ecosystem with C# language. It will enable more people to be able to do it.

At the moment people use Blazor's IJSObjectReference to achieve the same goals.
It's also generating lot of proxies. The method calls are also dynamic via bound string names.
But it is applicable only to Blazor, not to NodeJS and not in Uno, for example.

@Thaina
Copy link

Thaina commented Nov 28, 2022

I think there is 2 parts about performance

Performance on marshalling and proxy between C# and js is felt normal and unavoidable anyway so it more likely acceptable

But performance on Microsoft.CSharp and Linq.Expression from the usage of dynamic is not what most people expect. Also it seem like it will make the feature unuseable in AoT scenario, especially in unity

@pavelsavara
Copy link
Member Author

Then could we at least implement IDictionary and have Invoke function for whenever the JSObject is function?

I think this is actionable idea.

IDictionary<string, object> yes we could do that I think. Created separate issue

function: will need more design. Create separate issue

@SteveSandersonMS
Copy link
Member

It's interesting this comes up again :) @MackinnonBuck has previously explored this area in https://github.com/aspnet/AspLabs/tree/main/src/DynamicJS, so it would be worth comparing with that, and the discussions that surround it. Mackinnon's approach takes this a little further by batching the JS operations until a .NET evaluation is required, which has pros and cons.

We could add some indirection like JSObject.ToDynamic() extension method.

Yes, that sounds reasonable, especially if it goes into an external package you have to opt into, so there's a point of discovery where we can communicate details about what this is good for and what costs you need to know about.

The alternative is generators of strongly typed proxies and dealing with type-system mismatch there.

Are there particular cases where the type system is a blocker? I would have thought that ultimately it would be better if we could provide a .NET representation of the JS browser API surface since then you get proper code completions etc rather than the pre-TypeScript experience of just hoping the developer types API names correctly.

I appreciate the codegen approach is much more expensive for us to implement than mapping dynamic into JS calls. But if we did pay this cost it could be more of an ultimate solution since the codegen could project the same JS IDL API surface onto a range of different backend implementations for different hosting environments.

In both cases the OOP interaction would be making lot of small calls over the boundary. And both solutions will generate lot of GC traffic for both VMs. Overall the efficiency is out of window anyway.

That is partly why Mackinnon implemented batching (the other main reason was to make it practical for use in out-of-process scenarios like Blazor Server or WebView). However batching comes at the cost of changing exception throwing/catching semantics, which in turn was one of the main reasons we held back on the whole approach.

But it is applicable only to Blazor, not to NodeJS and not in Uno, for example.

Technically we can make Blazor's JS interop mechanism work on Node if we want - the IJSRuntime/etc APIs were always designed to be decoupled from Blazor and hence ship in separate packages. I'm not trying to push that angle, just saying it's possible, to have a more complete perspective. It has its own separate set of pros and cons, of course!

It's great to see experiments and ideas, @pavelsavara!

@Thaina
Copy link

Thaina commented Nov 28, 2022

As for me I think approach of codegen with typescript is perfect solution for giving intellisense. As the typescript already become standard for writing type definition and we could reuse whole typescript language server protocol

Is it possible have an experimental branch about this feature after we have #78905 and #78906 implemented ?

@pavelsavara
Copy link
Member Author

Is it possible have an experimental branch about this feature after we have #78905 and #78906 implemented ?

I believe that generated proxies should be more strongly typed than very dynamic approach we would use in #78905 and #78906.
So, it would not be dependent on it.

Sure we could have experimental branch. Feel free to give it a try @Thaina
But I could not promise any timeline or commitment to a particular design from my side at this moment .

@lambdageek
Copy link
Member

lambdageek commented Nov 28, 2022

A dynamic JSObject would be pretty compelling together with dotnet watch/Hot Reload for quick and dirty prototyping.

But it would be nice if there was an offramp once you're done hacking to a performant API without too much rewriting.

(But IDictionary / Invoke would also be pretty close, although a little bit clunkier)

@Thaina
Copy link

Thaina commented Nov 28, 2022

Is it possible have an experimental branch about this feature after we have #78905 and #78906 implemented ?

I believe that generated proxies should be more strongly typed than very dynamic approach we would use in #78905 and #78906. So, it would not be dependent on it.

Sure we could have experimental branch. Feel free to give it a try @Thaina But I could not promise any timeline or commitment to a particular design from my side at this moment .

I felt I don't have enough ability to make a direct proxy without wrapping JSObject and invoke which is why I would like to wait for those first

@pavelsavara
Copy link
Member Author

Are there particular cases where the type system is a blocker? I would have thought that ultimately it would be better if we could provide a .NET representation of the JS browser API surface since then you get proper code completions etc rather than the pre-TypeScript experience of just hoping the developer types API names correctly.

I don't know for sure yet. But TS is very powerful language. What you can do with generic constrains is scary to me right now.

I imagine that C# version of this API would be what people really want
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts

If we do it in relaxed way, it would marshal everything as any - JSObject, number, string it would not add much value over dynamic.

If we want to do it right, so that the generic factories to return parametrized generic proxies, it's kind of type-system rocket science to me.

I felt I don't have enough ability to make a direct proxy without wrapping JSObject and invoke which is why I would like to wait for those first.

I was thinking about the API which the current codegen uses.

@pavelsavara pavelsavara closed this Dec 7, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Jan 6, 2023
@pavelsavara pavelsavara deleted the wasm_jsobject_dynamic branch September 2, 2024 15:29
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants