elringus / bootsharp Goto Github PK
View Code? Open in Web Editor NEWCompile C# solution into single-file ES module with auto-generated JavaScript bindings and type definitions
Home Page: https://bootsharp.com
License: MIT License
Compile C# solution into single-file ES module with auto-generated JavaScript bindings and type definitions
Home Page: https://bootsharp.com
License: MIT License
Hi @elringus ,
There is a problem with exposing .Net enums to the TypeScript. For example, I have an enum in C# code like:
public enum Severity
{
Warning = 1,
Error = 2
}
Values are missed when it's translated into TS like:
export enum Severity {
Warning,
Error
}
Moreover, when I try to pass this like that:
dotnet.Something.someFunction({ severety: SomeNamespace.Severity.Error });
it fails with serialization exception, so I have to pass it like
dotnet.Something.someFunction({ severety: 2 });
I can try to prepare a PR to fix that, but want to discuss the details first
Starting with .NET 6 the runtime uses native platform API to generate random numbers: https://docs.microsoft.com/en-us/dotnet/api/system.guid.newguid?view=net-6.0#remarks
In browser crypto.getRandomValues
is utilized by the runtime and for other environments we are using a naive Math.random
shim at the moment: https://github.com/Elringus/DotNetJS/blob/97647de97a9ffe0aafdc1941a947f79f9816b9d8/JavaScript/dotnet-runtime/src/mono.ts#L39
As mentioned in the documentation, the function does not provide cryptographically secure random numbers.
While it's probably not possible (?) to implement proper strong entropy generation without access to the native APIs, we have to at least improve current naive solution.
Related: #2
Due to #68 compiler now require #nullable enable
defines when nullable reference type annotations are used in the auto-generated files.
I'm not aware of a reliable way to detect whether a nullable annotation is used in the analyzed syntax tree, so the generators always emit #nullable
defines (#71), which could cause issues when the consumer doesn't have nullable reference types enabled.
If anyone have any idea how to detect whether the nullable define should be emitted (eg, by checking whether a nullable annotation is used anywhere in the analyzed syntax tree):
— please let us know here.
Once multi-threading is available in .NET's wasm-browser, add an option to enable it with the packager.
Tracking: dotnet/runtime#68162
I have the following C# code:
[JSInvokable]
public static EquationSystem.SolveResult Move(IEntity entity, Vector3 pos)
{
var editor = GetEditor();
var moveTool = new MoveTool();
return moveTool.Move(entity, editor.ActiveFeature, pos);
}
Vector3 is a struct from System.Numerics.Vectors.dll.
This renders the following typescript:
export namespace Bindings {
export function Move(entity: Bindings.IEntity, pos: any): Bindings.SolveResult;
}
Do you know why pos
is of type any
?
To use this library with Deno, ESM would be required as Deno does not support UMD.
And the easiest thing would be just to export also a dotnet.mjs file (with esm code inside).
The following code leads to an invalid TypeScript type definition:
namespace Repro;
public class GenericType<T>
{
public T Value { get; set; }
}
public static partial class Program
{
public static void Main()
{
}
[JSInvokable]
public static void Function(GenericType<string> param)
{
}
}
This generates the following definitions in dotnet.d.ts
:
export namespace Repro {
export class GenericType`1 {
value?: string;
}
}
export namespace Repro {
export function Function(model: Repro.GenericType`1): void;
}
I think it should be possible to generate generic type definitions. If it's not, however, a mechanism that would allow me to specify that a given type should just be typed as any
would also work for me. First I thought about an attribute, but that wouldn't work for third party types, so it's probably not the best solution.
As per the documentation, it should be possible to expose JavaScript functions under scopes:
We are currently assigning them to the global object, which is not ideal:
https://github.com/Elringus/DotNetJS/blob/97647de97a9ffe0aafdc1941a947f79f9816b9d8/DotNet/Packer.Test/LibraryTest.cs#L73
I ran the Sample/React project and tried to keep the donut from lagging under stress. Enabling CreateWorker didn't work for me :/ After which I saw your comment: "After some testing I've figured worker interaction layer adds too much complexity and makes debugging harder. It'll make more sense and will probably be more performant to use threading on C# side (#79)."
So I tried using Task on C# side.
public async Task StartStress()
{
cts?.Cancel();
cts = new CancellationTokenSource();
await Task.Run(() => Stress(cts.Token));
}
private async Task Stress(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
var time = DateTime.Now;
await Task.Run(() => ComputePrime(frontend.GetStressPower()), token);
frontend.NotifyStressComplete((DateTime.Now - time).Milliseconds);
await Task.Delay(1, token);
}
}
I'm not strong in multithreading, but I thought this should work. Understandably, it didn't work
How can I run the process on a background thread? is it possible now? I will be very grateful for the answer
I've noticed that methods are not generated for classes other than Program
. So, if for example Program
has a method that returns an object of type Foo
, and Foo
has a method Bar()
, Bar()
isn't generated for the typescript definition.
Is that by design or am I doing something wrong?
Can you add a // <auto-generated />
comment at the top of the generated files?
According to this issue, this is the way to go to avoid triggering StyleCop rules.
From what I can see here https://github.com/Elringus/DotNetJS/blob/17c169b58bd6e66fbba0d54c690f5ca9c2cc9742/DotNet/Packer/DeclarationGenerator/TypeDeclarationGenerator.cs#L129 we currently only generate auto-implemented properties.
Is there any reason to restrict generating only those instead of all properties like get-only, set-only, or get-set-properties with implemented bodies (not auto-implemented)?
It looks like at some point (.NET 7?) a new JS interop layer will be introduced:
Hi, thank you so much for creating this helpful project! I am trying to implement the simple HelloWorld example from the ReadMe in my Typescript project, and I'm getting an error. Here are the relevant files:
HelloWorld.cs:
using System;
using DotNetJS;
using Microsoft.JSInterop;
namespace HelloWorld
{
class Program
{
public static void Main ()
{
var hostName = JS.Invoke<string>("GetName");
Console.WriteLine($"Hello {hostName}, DotNet here!");
}
[JSInvokable]
public static string GetName () => "DotNet";
}
}
HelloWorld.csproj:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetJS" Version="*"/>
</ItemGroup>
</Project>
MyTsFunction:
function myTsFunction(): string {
const dotnet = require("./bin/dotnet");
dotnet.HelloWorld.GetHostName = () => "Node.js";
(async function () {
await dotnet.boot();
const guestName = dotnet.HelloWorld.GetName();
console.log(`Welcome, ${guestName}! Enjoy your module space.`);
})();
return "";
}
I ran dotnet publish to create the dotnet.js file and I am able to import the module, but there is an error on the " dotnet.HelloWorld.GetHostName = () => "Node.js"; " line:
Cannot set properties of undefined (setting 'GetName')
Do you have any ideas on how I could fix this?
It is not clear whether the .NET runtime WASM module have to be terminated and/or if it can be re-initialized.
There appears to be emscripten_force_exit
function in the module, but it throws an error:
https://github.com/Elringus/DotNetJS/blob/97647de97a9ffe0aafdc1941a947f79f9816b9d8/JavaScript/dotnet-runtime/src/wasm.ts#L25
Is someone have any related info, please let us know here.
If the project gets published with <EmbedBinaries>false</EmbedBinaries>
the generated dotnet.d.ts
file does not match the requested function signature.
It is generating this line:
export declare function boot(): Promise<void>;
instead of:
export declare function boot(bootData: BootData): Promise<void>;
Seems to be about this line:
https://github.com/Elringus/DotNetJS/blob/4c7235770c4b78a43276b86b52e195f0903a23a0/DotNet/Packer/DeclarationGenerator/DeclarationGenerator.cs#L69
I don't know when it gets called.
The Sample project throws an error if published:
$ git clone https://github.com/Elringus/DotNetJS.git
$ cd DotNetJS/Samples/HelloWorld/
$ dotnet publish
Microsoft (R) Build Engine version 17.0.0+c9eb9dd64 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
Restored /workspaces/DotNetJS/Samples/HelloWorld/Project/HelloWorld.csproj (in 428 ms).
CSC : warning CS8032: An instance of analyzer Generator.SourceGenerator cannot be created from /var/nuget/dotnetjs/0.9.1/analyzers/dotnet/cs/Generator.dll : Could not load file or assembly 'Microsoft.CodeAnalysis, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. The system cannot find the file specified.. [/workspaces/DotNetJS/Samples/HelloWorld/Project/HelloWorld.csproj]
/workspaces/DotNetJS/Samples/HelloWorld/Project/Program.cs(19,34): error CS8795: Partial method 'Program.GetHostName()' must have an implementation part because it has accessibility modifiers. [/workspaces/DotNetJS/Samples/HelloWorld/Project/HelloWorld.csproj]
dotnet --version
6.0.100
I'm working on a VS Code extension and writing the base implementation in .Net and calling from the extension in Typescript. I'm having trouble accessing local files from the .Net code using System.IO.* API. E.g. calling File.Exists() on a local Windows file path returns false for a valid path. I'm guessing that I don't have permission to access the file from the running process or the path needs to be modified. Any ideas?
Instead of JSON, explore marshaling via arrays, similar to Embind (https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#value-types), eg:
public record Record (string Str, int Int, bool Bool, Record? Other);
[JSExport]
private static void ReceiveRecord ([JSMarshalAs<JSType.Any>] object raw)
{
var record = Unmarshal((object?[])raw);
Console.WriteLine($"Record({record.Str}, {record.Int}, {record.Bool}, Record({record.Other?.Str},
{record.Other?.Int}, {record.Other?.Bool}))");
static Record Unmarshal (object?[] raw) => new(
(string)raw[0]!,
(int)(double)raw[1]!,
(bool)raw[2]!,
raw[3] != null ? Unmarshal(raw[3..7]) : null
);
}
exports.Program.ReceiveRecord(["foo", 1, true, "bar", 2, false, null]);
And the other way around:
[JSExport] [return: JSMarshalAs<JSType.Any>]
private static object GetRecord ()
{
return new object?[] { "foo", 5, true, new object?[] { "bar", 6, false, null } };
}
— returns in JavaScript: ["foo",5,true,["bar",6,false,null]]
.
Should probably be postponed until #138 is solved; otherwise, we'll break interop with tasks of custom data types, that are currently serialized to JSON.
We can actually send arrays as objects:
[JSExport] [return: JSMarshalAs<JSType.Promise<JSType.Any>>]
private static async Task<object> GetRecordAsync ()
{
await Task.Delay(1);
return new object?[] { "foo", 7, true, new object?[] { "bar", 8, false, null } };
}
— the task value is transferred to JavaScript as ["foo",7,true,["bar",8,false,null]]
.
Currently, it's required to apply [JSNamespace]
attribute to each assembly that has JS bindings and to the entry assembly.
That is because source generator gets the attribute from the assembly it's currently generating code for:
While packer gets the attribute from the entry assembly and applies the rules for all the other assemblies:
Ideally, we have to find a way to make generator get the attribute from the entry assembly (if that's possible) or make packer resolve the attributes for each assembly individually to at least prevent cases when generated C# bindings are not in sync with JS counterparts.
Currently, we can detect when a property, method argument and method return type is explicitly marked as nullable (either Nullable<>
or has ?
in nullable context) and generate corresponding TypeScript declaration with either ?
or undefined
modifier:
However, I'm not sure if it's possible to detect when a collection element type has nullable modifier, eg string?[]
or List<string?>
. NullabilityInfoContext doesn't seem to have a way to detect that.
If anyone have any idea how to infer nullability of collection and array element types, please let us know here.
If anyone is aware of a github action that will lint the repo files to comply with .editorconfig, please let us know here.
We've tried https://github.com/megalinter/megalinter and https://github.com/github/super-linter configured to run only https://github.com/editorconfig-checker/editorconfig-checker, but they're both very slow.
.NET's JS interop currently is only able to marshal types with single-level nesting:
— hence we have to handle otherwise natively-supported arrays (eg, Task<byte[]>
) in a special manner.
Lift the array to object
and marshal as JSType.Any
:
[JSExport] [return: JSMarshalAs<JSType.Promise<JSType.Any>>]
private static async Task<object> GetArrayAsync ()
{
await Task.Delay(1);
return new[] { "foo", "bar", "baz" };
}
This is currently employed under the hood and doesn't require any action from end-user, but costs an extra allocation when unmarshalling from JS to C#.
Wrap the call into two: first to wait for the async operation, second to get the array in a blocking manner, eg:
[JSFunction] Task OpenFile (string uri);
[JSFunction] byte[] GetOpenFileContent (string uri);
Task<byte[]> ReadFile (string uri)
{
await OpenFile(uri);
return GetOpenFileContent(uri);
}
Explore an option to include C# XML docs (when present) to the generated TS type declarations.
After generating the WASM and such, is it possible to attach the C# debugger to it? Is there some docs available for that?
Hello:
I want to know if I can find how can I call some Node.js script from C#, which call a npm package called ccxt, and return the truncated strings to C#. And one step further, how can I call similar Node.js script and pass some parameter and get the results back to C#.
My OS: Window 10 Pro (version 21H2)
Node.js: version: 19.0.0
CCXT: version: 2.1.33
The following is node.js script:
D:\nodejs\CCXT_Exchanges>type ccxt_all_exchanges.js
'use strict';
const ccxt = require('ccxt');
console.log(ccxt.version);
var all_exchanges = ccxt.exchanges
console.log(all_exchanges)
D:\nodejs\CCXT_Exchanges>
The output for running the node.js script:
D:\nodejs\CCXT_Exchanges>node ccxt_all_exchanges.js
2.1.33
[
'aax', 'alpaca', 'ascendex',
'bequant', 'bibox', 'bigone',
'binance', 'binancecoinm', 'binanceus',
'binanceusdm', 'bit2c', 'bitbank',
'bitbay', 'bitbns', 'bitcoincom',
'bitfinex', 'bitfinex2', 'bitflyer',
'bitforex', 'bitget', 'bithumb',
'bitmart', 'bitmex', 'bitopro',
'bitpanda', 'bitrue', 'bitso',
'bitstamp', 'bitstamp1', 'bittrex',
'bitvavo', 'bkex', 'bl3p',
'blockchaincom', 'btcalpha', 'btcbox',
'btcex', 'btcmarkets', 'btctradeua',
'btcturk', 'buda', 'bw',
'bybit', 'bytetrade', 'cex',
'coinbase', 'coinbaseprime', 'coinbasepro',
'coincheck', 'coinex', 'coinfalcon',
'coinmate', 'coinone', 'coinspot',
'crex24', 'cryptocom', 'currencycom',
'delta', 'deribit', 'digifinex',
'exmo', 'flowbtc', 'fmfwio',
'ftx', 'ftxus', 'gate',
'gateio', 'gemini', 'hitbtc',
'hitbtc3', 'hollaex', 'huobi',
'huobijp', 'huobipro', 'idex',
'independentreserve', 'indodax', 'itbit',
'kraken', 'kucoin', 'kucoinfutures',
'kuna', 'latoken', 'lbank',
'lbank2', 'liquid', 'luno',
'lykke', 'mercado', 'mexc',
'mexc3', 'ndax', 'novadax',
'oceanex', 'okcoin', 'okex',
'okex5', 'okx', 'paymium',
'phemex',
... 20 more items
]
D:\nodejs\CCXT_Exchanges>
The issue here is that console.log only shows the first 100 items, there are 20 more items will be returned. I want to use ClearScript to run the above node.js script and return all 120 items to C#.
How can I do this?
One step further, can I pass variable to async function using CCXT?
D:\nodejs\CCXT_Binance>type Binance1PairOrderBook.js
const ccxt = require('ccxt');
console.log(ccxt.version);
if (process.argv.length != 3)
{
console.error('Usage: BTC/USDT');
console.error('Please try again!');
process.exit(1);
}
const [nodejs, script1, pair1] = process.argv;
let exchange = new ccxt.binance
({
'adjustForTimeDifference': true,
defaultType: 'spot',
'verbose': false
});
const pair1OrderBook = async (pair1) => {
try
{
await exchange.loadMarkets();
const orderbook1 = await exchange.fetchOrderBook(pair1);
if (typeof orderbook1 !== 'undefined')
{
const json_orderbook = JSON.stringify(orderbook1);
console.log(json_orderbook);
}
else
{
const obj_orderbook0 = ({ 'Symbol': pair1, 'Data': [] });
const json_orderbook0 = JSON.stringify(obj_orderbook0);
console.log(json_orderbook0);
}
}
catch (err)
{
console.error(err)
}
};
pair1OrderBook(pair1);
D:\nodejs\CCXT_Binance>
To use this code, call this with one cryptocurrency trading pair, the biggest pair is: BTC/USDT, but the output contains too many rows, so calling it with not very active pairs, like this:
D:\nodejs\CCXT_Binance>node Binance1PairOrderBook.js FXS/USDT
Or some pairs, which do exist, but no trade volume here: ETH/USDC, like the following:
D:\nodejs\CCXT_Binance>node Binance1PairOrderBook.js ETH/USDC
2.1.33
(node:22912) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time
(Use node --trace-warnings ...
to show where the warning was created)
{"symbol":"ETH/USDC","bids":[],"asks":[],"nonce":2090539859}
D:\nodejs\CCXT_Binance>
Please advise on if I can use the repo to do the job: call Node.js from C# WinForms App?
Thanks,
When I do dotnet publish
for the Hello World sample on Window, everything works, and the UMD library is published. When I run it on a Mac, I get error CS8795: Partial method 'Program.GetHostName()' must have an implementation part because it has accessibility modifiers.
Severity Code Description Project File Line Suppression State
Error The "PublishDotNetJS" task failed unexpectedly.
System.IO.DirectoryNotFoundException: Could not find a part of the path '...\bin\Release\net6.0\publish\wwwroot_framework'.
at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
at System.IO.FileSystemEnumerableIterator1.CommonInit() at System.IO.FileSystemEnumerableIterator
1..ctor(String path, String originalUserPath, String searchPattern, SearchOption searchOption, SearchResultHandler`1 resultHandler, Boolean checkHost)
at System.IO.Directory.GetFiles(String path, String searchPattern)
at DotNetJS.Packer.ProjectMetadata.LoadAssemblies(String directory)
at DotNetJS.Packer.PublishDotNetJS.Execute()
at Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute()
at Microsoft.Build.BackEnd.TaskBuilder.d__26.MoveNext() vNextDashboard.Pathways 0
The folder is not:
....\bin\Release\net6.0\publish\wwwroot_framework
It's
...\bin\Release\net6.0\wwwroot_framework
I have not found a place to set a different location.
Anything that uses IJSObjectReference
is set as any
in the declaration files.
We could introduce an optional attribute JSObjectInterface
that would add the necessary metadata for the declarations generator to expose the type correctly.
public sealed class JSObjectInterfaceAttribute : Attribute
{
public JSInterfaceAttribute (Type type) { }
}
public interface IMyInterface
{
void Method (string str);
}
public class Container
{
[JSInterface(typeof(IMyInterface))]
public IJSObjectReference Prop { get; set }
public void Test ([param: JSObjectInterface(typeof(IMyInterface))] IJSObjectReference @param) { }
}
So we could have TS declarations in the following format
export interface IMyInterface {
Method(str: string): void;
}
Another way would be to introduce generic interfaces that implement IJSObjectReference
, but there are a lot of flavors, like IJSUnmarshalledObjectReference
, IJSInProcessObjectReference
, so while it adds some type safety for C#, the maintenance of those higher interfaces might be very costly.
We are currently using a custom .NET runtime build and JS wrapper post-processing to support webpack UMD target. This limits the options when building the projects, such as AOT and module trimming.
There is an on-going work at the .NET runtime main branch to modularize the WASM runtime and make it work outside of browser, eg:
dotnet/runtime#61313
dotnet/runtime#62292
dotnet/runtime#47336
At some point, it may become possible to use the module distributed with the runtime SDK, which will resolve the limitations.
If someone happen to know an alternative solution to make current (.NET 6) WASM compatible with UMD target, please let us know here. Sharing updates regarding changes in the .NET runtime that could help with the issue is also appreciated.
Currently blocked by:
Publishing with -o, --output <OUTPUT_DIR>
option breaks the PublishDotNetJS
task.
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetJS" Version="0.23.1" />
</ItemGroup>
</Project>
using Microsoft.JSInterop;
Console.WriteLine("Hello, World!");
[JSInvokable]
static string Test(string value) => value;
dotnet publish -c "Release" -o "D:\ExampleApp\.deploy\test"
...\DotNetJS.targets(23,9): error MSB4018: The "PublishDotNetJS" task failed unexpectedly.
...\DotNetJS.targets(23,9): error MSB4018: System.IO.DirectoryNotFoundException: Could not find a part of the path 'D:\ExampleApp\ExampleApp\bin\Release\net6.0\publish\wwwroot\_framework'.
...\DotNetJS.targets(23,9): error MSB4018: at System.IO.Enumeration.FileSystemEnumerator`1.CreateDirectoryHandle(String path, Boolean ignoreNotFound)
...\DotNetJS.targets(23,9): error MSB4018: at System.IO.Enumeration.FileSystemEnumerator`1.Init()
...\DotNetJS.targets(23,9): error MSB4018: at System.IO.Enumeration.FileSystemEnumerable`1..ctor(String directory, FindTransform transform, EnumerationOptions options, Boolean isNormalized)
...\DotNetJS.targets(23,9): error MSB4018: at System.IO.Enumeration.FileSystemEnumerableFactory.UserFiles(String directory, String expression, EnumerationOptions options)
...\DotNetJS.targets(23,9): error MSB4018: at System.IO.Directory.InternalEnumeratePaths(String path, String searchPattern, SearchTargetsearchTarget, EnumerationOptions options)
...\DotNetJS.targets(23,9): error MSB4018: at System.IO.Directory.GetFiles(String path, String searchPattern, EnumerationOptions enumerationOptions)
...\DotNetJS.targets(23,9): error MSB4018: at Packer.TypeUtilities.CreateLoadContext(String directory)
...\DotNetJS.targets(23,9): error MSB4018: at Packer.NamespaceBuilder.CollectConverters(String outDir, String entryAssembly)
...\DotNetJS.targets(23,9): error MSB4018: at Packer.PublishDotNetJS.CreateNamespaceBuilder()
...\DotNetJS.targets(23,9): error MSB4018: at Packer.PublishDotNetJS.GenerateSources()
...\DotNetJS.targets(23,9): error MSB4018: at Packer.PublishDotNetJS.Execute()
...\DotNetJS.targets(23,9): error MSB4018: at Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute()
...\DotNetJS.targets(23,9): error MSB4018: at Microsoft.Build.BackEnd.TaskBuilder.ExecuteInstantiatedTask
While packing everything to a single .js library is convenient and even required in some cases (eg, for VS Code web extensions), it adds about 30% of extra size due to binary->base64 conversion of the WASM and DLLs.
We should add a build option to not pack the binary boot data with the emitted .js and instead allow the data to be side-loaded by the consumer; this way the binary files can be streamed directly from the server to optimize the traffic/initial load time.
Skipping boot function override should be enough to achieve that (bootData
would be provided by the consumer): https://github.com/Elringus/DotNetJS/blob/a126393aed63581ec3078db0a382dffc84adfad2/DotNet/Packer/LibraryGenerator.cs#L17-L25
Given there are multiple assemblies that require JavaScript interop (contain[JSInvokable]
or [JSFunction]
attributed methods), only the entry assembly is loaded, while when trying to invoke methods from the others, Assembly not loaded
exception is thrown.
We are currently using following hack to force-load such assemblies:
https://github.com/Elringus/DotNetJS/blob/97647de97a9ffe0aafdc1941a947f79f9816b9d8/JavaScript/dotnet-runtime/test/csharp/Test.Main/Program.cs#L31
If the hack is removed, the JS tests that use methods from Test.Types
assembly will fail.
We have to figure if that's by design and/or find a more elegant way to force-load the assemblies.
If someone have any related info, please let us know here.
Just hope that maybe somebody else came across the same error:
error MSB4062: The "Bootsharp.Publish.BootsharpEmit" task could not be loaded from the assembly C:\Users\dnhb\.nuget\packages\bootsharp\0.1.3\build\..\tasks\Bootsharp.Publish.dll. Could not load file or assembly 'System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified. Confirm that the <UsingTask> declaration is correct, that the assembly and all its dependencies are available, and that the task contains a public class that implements Microsoft.Build.Framework.ITask.
4>Done building target "BootsharpEmit" in project "Project.csproj" -- FAILED
dotnet build
is running just fine. Only VS gives problems.
Blazor provides APIs for unmarshalled JavaScript interop: https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet#unmarshalled-javascript-interop
We have to check if it's working with DotNetJS and implement associated tests: https://github.com/Elringus/DotNetJS/blob/97647de97a9ffe0aafdc1941a947f79f9816b9d8/JavaScript/dotnet-runtime/test/interop.js#L138
We are currently patching .NET's and Emscripten's JavaScript modules to make them compatible with JavaScript runtimes and bundlers:
— this is fragile and will most likely break with each new .NET release and/or WASM workload update.
Hopefully, .NET will at some point ensure the modules are compatible out of the box, so we won't have to do this:
Originally posted by CADBIMDeveloper January 6, 2024
Hi @elringus ,
Seems, I'm a bit annoying last days, sorry about that :-(
I faced an issue when the code generated by bootsharp on Windows was not working on Linux raising the following error:
MONO_WASM: The argument 'filename' must be a file URL object, file URL string, or absolute path string. Received 'file://dotnet.native.wasm'
TypeError [ERR_INVALID_ARG_VALUE]: The argument 'filename' must be a file URL object, file URL string, or absolute path string. Received 'file://dotnet.native.wasm'
at new NodeError (node:internal/errors:405:5)
at Module.createRequire (node:internal/modules/cjs/loader:1493:13)
...
I believe it happens because different code is generated on Windows and Linux machines:
On Windows:
...
.then((e=>e.createRequire("file://dotnet.native.wasm")))
...
On Linux:
...
.then((e=>e.createRequire("file:///dotnet.native.wasm")))
...
I solved it generating separated versions on Windows and Linux.
I'm not sure if it can be fixed, it's probably came from browser-wasm
workload. What do you think? Thank you!
Hi,
I tried implementing the bootData
section for your example project:
// Providing implementation for 'GetHostName' function declared in 'HelloWorld' C# assembly.
dotnet.HelloWorld.GetHostName = () => "Browser";
async function loadfile(path) {
response = await fetch(path);
// Examine the text in the response
buffer = await response.arrayBuffer();
return Base64.fromUint8Array(new Uint8Array(buffer));
}
window.onload = async function () {
allfiles = [
"Microsoft.JSInterop.WebAssembly.dll",
"System.Collections.Concurrent.dll",
"System.Private.Runtime.InteropServices.JavaScript.dll",
"System.Collections.dll",
"Microsoft.AspNetCore.Components.Web.dll",
"System.Memory.dll",
"Microsoft.JSInterop.dll",
"Microsoft.AspNetCore.Components.WebAssembly.dll",
"System.Runtime.dll",
"System.Text.Json.dll",
"Microsoft.AspNetCore.Components.dll",
"System.Private.Uri.dll",
"System.Private.CoreLib.dll",
"System.Runtime.CompilerServices.Unsafe.dll",
"System.Console.dll",
"DotNetJS.dll",
"System.Text.Encodings.Web.dll",
"HelloWorld.dll",
];
assemblies = [];
for (let i = 0; i < allfiles.length; i++) {
assemblies.push({
name: allfiles[i],
data: await loadfile("Project/bin/Debug/net6.0/" + allfiles[i]),
});
}
// Booting the DotNet runtime and invoking entry point.
const bootData = {
wasm: await loadfile("Project/bin/Debug/net6.0/dotnet.wasm"),
assemblies: assemblies,
entryAssemblyName: "HelloWorld.dll",
};
await dotnet.boot(bootData);
// Invoking 'GetName()' C# method defined in 'HelloWorld' assembly.
const guestName = dotnet.HelloWorld.GetName();
console.log(`Welcome, ${guestName}! Enjoy your global space.`);
};
But I always get this error, any idea why?
dotnet.js:488 Uncaught (in promise) RuntimeError: abort(TypeError: WebAssembly.instantiate(): Import #0 module="a" error: module is not an object or function). Build with -s ASSERTIONS=1 for more info.
at abort (dotnet.js:488:25)
at dotnet.js:542:25
Remark:
I also tried the Project/bin/Debug/net6.0/publish/wwwroot/_framework/
Path.
Hi! Thanks for the great library!
Since CodeGen was merged, I switched to the emitted declaration files instead of using manually created ones based on the source code, but the issue is, the nesting is wrong!
Right now the declarations are emitted like this:
export declare const dotnet: {
BannerlordModuleManager: { }; // The C# bindings
BootStatus: typeof BootStatus;
...
};
But dotnet.js exposes the variable via exports.BannerlordModuleManager
instead of exports.dotnet.BannerlordModuleManager
So the correct declaration would be this:
export declare const BannerlordModuleManager: { }; // The C# bindings
export declare const dotnet: {
BootStatus: typeof BootStatus;
...
};
PS:
Writing it here because Discussions aren't setup. Since we both are from Russia, I can't setup PayPal donations via Ko-Fi because Russia<-->Russia transactions are banned. Do you know any way to bypass the restriction?
Hey, thank you for this awesome project! I'm currently trying to get some existing .NET code to run in a VS Code web extension, so it came just at the right time for me, too.
I got the basics to work, however a call to Guid.NewGuid()
results in an exception:
System.AggregateException: AggregateException_ctor_DefaultMessage (Arg_CryptographyException)
---> System.Security.Cryptography.CryptographicException: Arg_CryptographyException
at Interop.GetCryptographicallySecureRandomBytes(Byte* , Int32 )
at System.Guid.NewGuid()
at Program.Main(String[] args)
Exception_EndOfInnerExceptionStack
Do you have any idea why this happens and how it can be prevented? It works in a normal Blazor app.
It might be related to this change:
On non-Windows platforms, starting with .NET 6, this function calls the OS's underlying cryptographically secure pseudo-random number generator (CSPRNG) to generate 122 bits of strong entropy. In previous versions of .NET, the entropy is not guaranteed to be generated by a CSPRNG.
Hi!
I found that after bundling, the created module does not work (dotnet.boot()
crashes). This is due to the usage of undeclared identifiers assigned in certain functions. This shouldn't be a problem, but since some bundlers seem to tack on 'use strict';
where they shouldn't, it becomes an issue, and fixing it shouldn't cause any problems.
The offending functions are:
_js_to_mono_obj
: result
is undeclared
bind_method
: bodyJs
is underclared
_handle_exception_and_produce_result_for_call
: result
is undeclared
While Blazor provide APIs for raw byte streaming between .NET and JavaScript (doc), .NET to JS streaming relies on browser-only Response
type:
Associated calls in the runtime:
https://github.com/dotnet/aspnetcore/blob/release/6.0/src/Components/Web.JS/src/GlobalExports.ts#L80
https://github.com/dotnet/aspnetcore/blob/release/6.0/src/Components/Web.JS/src/StreamingInterop.ts#L24
We have to add environment-agnostic streaming implementation and test it:
https://github.com/Elringus/DotNetJS/blob/97647de97a9ffe0aafdc1941a947f79f9816b9d8/JavaScript/dotnet-runtime/test/interop.js#L125
Hi again!
I used DotNetJS to create a library that exposes some common functions that we share across C# executables and an Electron app to ensure the behaviour is identical.
The problem is, the electron's app (Vortex) is blocking eval
execution due to it's CSP. I did earlier some investigation about this, but I would like to double check! Is eval
critical for the runtime to work or is it used only for debugging? Could we strip it's usage with some conditionals?
Hi @elringus ,
Bootsharp makes malformed bindings.g.js
file in some cases when the first namespace name is a part of the second namespace name and parseAst.js
fails with a message like Expected ',' got 'export'
.
I manged to reproduce this issue with the following toy example:
namespace BootSharpBackendCore.Input;
public enum Foo
{
Value1 = 0,
Value2 = 1
}
using BootSharpBackendCore.Input;
namespace BootSharpBackendCore.Output;
public record Bar(Foo Foo);
namespace BootSharpBackendCore;
public static class Facade
{
public static IReadOnlyList<Bar> Test(Foo foo) => new[] { new Bar(foo) };
}
and a project with JSInvokable
:
using Bootsharp;
using BootSharpBackendCore;
using BootSharpBackendCore.Input;
using BootSharpBackendCore.Output;
namespace BootSharpBackend;
public static partial class Program
{
public static void Main()
{
}
[JSInvokable]
public static IReadOnlyList<Bar> Test(Foo foo) => Facade.Test(foo);
}
dotnet publish
command will fail, but some of artifacts will remain, so, the bin\Release\net8.0\browser-wasm\AppBundle\_framework\bindings.g.js
file will have something like that:
import { exports } from "./exports";
import { Event } from "./event";
function getExports () { if (exports == null) throw Error("Boot the runtime before invoking C# APIs."); return exports; }
function serialize(obj) { return JSON.stringify(obj); }
function deserialize(json) { const result = JSON.parse(json); if (result === null) return undefined; return result; }
export const BootSharpBackend = {
test: (foo) => deserialize(getExports().BootSharpBackend_Program.Test(serialize(foo)))
export const BootSharpBackendCore = {
Input: {
Foo: { "0": "Value1", "1": "Value2", "Value1": 0, "Value2": 1 }
}
};
Thank you!
Currently the readme has Program.cs as a non-public, when this is the case the library does not generate the web assembly code. This could lead people astray who are trying the tool out
Currently, when a [JSFunction]
-attributed method is invoked while the associated function is not implemented in JavaScript, a JSException
is thrown, but without any message to identify reason of the error: https://github.com/Elringus/DotNetJS/blob/f44665a9b7b4259d13469ef5ea928c7cdb6caff1/JavaScript/dotnet-runtime/test/packed.js#L26
We've tried initializing the bindings with function (...args) { throw Error("..."); }
instead of undefined
: https://github.com/Elringus/DotNetJS/blob/f44665a9b7b4259d13469ef5ea928c7cdb6caff1/DotNet/Packer/LibraryGenerator.cs#L71
— hoping the message will be propagated back to JavaScript, but .NET runtime throws back just the exception name with stacktrace and, more importantly, the runtime fails to invoke the function after such throw, even if it's assigned.
If anyone have any idea on how this can be solved, please let us know here.
Hi again,
I tested the library with angular and it is running into an error:
universalModuleDefinition:1 Uncaught ReferenceError: global is not defined
at universalModuleDefinition:1:1
at Object.9424 (universalModuleDefinition:1:1)
at __webpack_require__ (bootstrap:19:1)
at Module.1896 (home.component.html:14:263)
at __webpack_require__ (bootstrap:19:1)
at Module.721 (universalModuleDefinition:1:1)
at __webpack_require__ (bootstrap:19:1)
at Module.23 (app.component.html:6:8)
at __webpack_require__ (bootstrap:19:1)
at Module.8835 (environment.ts:16:71)
Which is expected as the angular team came to the conclusion to not support global in their webpack build for broad compatibility. angular/angular-cli#9827 (comment)
I think this library should not rely on some other process to supply the variable.
So going forward you could use:
Workaround is adding (window as any).global = window;
to polyfill.ts
Hi, thanks for this great project! I am trying to implement MozJpegSharp image compression based on the HelloWorld example from the ReadMe you provided, and I'm getting an error. Here are the relevant files:
using System;
using DotNetJS;
using Microsoft.JSInterop;
using MozJpegSharp;
namespace ImageCompression;
public partial class Program
{
// Entry point is invoked by the JavaScript runtime on boot.
public static void Main()
{
var hostName = GetHostName();
Console.WriteLine($"Hello {hostName}, DotNet here!");
}
[JSInvokable]
public static string CompressBase64(string base64, int quality)
{
var bytes = Helpers.Base64ToByteArray(base64);
var result = MozJpeg.Recompress(bytes.AsSpan(), quality, TJSubsamplingOption.Chrominance420, TJFlags.None);
return Helpers.ByteArrayToBase64(result);
}
}
ImageCommpression.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<EmitSourceMap>true</EmitSourceMap>
<EmitTypes>true</EmitTypes>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetJS" Version="0.3.12" />
<PackageReference Include="MozJpegSharp" Version="2.1.12" />
<PackageReference Include="MozJpegSharp.GdiPlus" Version="2.1.12" />
</ItemGroup>
</Project>
JS function
<script src="bin/dotnet.js"></script>
<script>
dotnet.ImageCompression.GetHostName = () => "Browser";
window.onload = async function () {
await dotnet.boot();
var data = "<SOME BASE64 IMAGE>";
const compressed = dotnet.ImageCompression.CompressBase64(data, 85);
console.log(`Image: , ${compressed}!`);
};
</script>
I ran dotnet publish
to create the dotnet.js file and I am able to import the module, but I receive an error in console:
Uncaught (in promise) Error: System.DllNotFoundException: turbojpeg
When I run same program as a console application everything is working fine.
Do you have any ideas what might be a problem?
Congrats!
You just got listed in the awesome-node directory!
https://github.com/sindresorhus/awesome-nodejs#cross-platform-integration
Regards, Robin
Hi @elringus ,
There is a problem with enum values handling in the bootsharp. Let's say, I have the following backend:
public enum Foo
{
Value1 = 1,
Value2 = 2
}
public static partial class Program
{
public static void Main ()
{
}
[JSInvokable]
public static Foo Test() => Foo.Value1;
}
It generates the following definitions:
const Global = {
...
test: () => deserialize(getExports().Program.Test()),
Foo: { "0": "Value1", "1": "Value2", "Value1": 0, "Value2": 1 }
};
So
console.log(Global.test() === Global.Foo.Value1);
prints false
Thank you!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.