Skip to content

Commit a5e0244

Browse files
authored
Node.js embedding APIs and demo app (microsoft#91)
1 parent 38dc16d commit a5e0244

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2367
-120
lines changed

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
<PackageVersion Include="System.Reflection.MetadataLoadContext" Version="6.0.0" />
1414
<PackageVersion Include="xunit" Version="2.4.2" />
1515
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
16+
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
1617
</ItemGroup>
17-
</Project>
18+
</Project>

README.md

Lines changed: 105 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,61 @@
1-
# Call .NET APIs from JavaScript
1+
# Node API for .NET: JavaScript + .NET Interop
22

3-
Call nearly any .NET APIs in-proc from JavaScript code, with high performance and TypeScript
4-
type-checking. The interop uses [Node API](https://nodejs.org/api/n-api.html) so it is compatible
5-
with any Node.js version (without rebuilding) or other JavaScript engine that supports Node API.
3+
This project enables advanced interoperability between .NET and JavaScript in the same process.
4+
5+
- Load .NET assemblies and call .NET APIs in-proc from a JavaScript application.
6+
- Load JavaScript packages call JS APIs in-proc from a .NET application.
7+
8+
Interop is high-performance and supports TypeScript type-definitions generation, async
9+
(tasks/promises), streams, and more. It uses [Node API](https://nodejs.org/api/n-api.html) so
10+
it is compatible with any Node.js version (without recompiling) or other JavaScript runtime that
11+
supports Node API.
612

713
:warning: _**Status: In Development** - Core functionality works, but many things are incomplete,
814
and it isn't yet all packaged up nicely in a way that can be easily consumed._
915

1016
[Instructions for getting started are below.](#getting-started)
1117

12-
### Minimal example
18+
### Minimal example - JS calling .NET
1319
```JavaScript
20+
// JavaScript
1421
const Console = require('node-api-dotnet').Console;
1522
Console.WriteLine('Hello from .NET!');
1623
```
24+
25+
### Minimal example - .NET calling JS
26+
```C#
27+
// C#
28+
[JSImport("global", "console")]
29+
interface IConsole { void Log(string message); }
30+
31+
var nodejs = new NodejsPlatform(libnodePath).CreateEnvironment();
32+
nodejs.Run(() => {
33+
var console = nodejs.Import<IConsole>();
34+
console.Log("Hello from JS!");
35+
});
36+
```
37+
1738
For more examples, see the [examples](./examples/) directory.
1839

1940
## Feature Highlights
20-
21-
### Dynamically load .NET assemblies
22-
.NET core library types are available directly on the main module. Additional .NET assemblies can
23-
be loaded by file path:
41+
- [Load and call .NET assemblies from JS](#load-and-call-net-assemblies-from-js)
42+
- [Load and call JavaScript packages from .NET](#load-and-call-javascript-packages-from-net)
43+
- [Generate TS type definitions for .NET APIs](#generate-ts-type-definitions-for-net-apis)
44+
- [Full async support](#full-async-support)
45+
- [Error propagation](#error-propagation)
46+
- [Develop Node.js addons with C#](#develop-nodejs-addons-with-c)
47+
- [Optionally work directly with JS types in C#](#optionally-work-directly-with-js-types-in-c)
48+
- [Automatic efficient marshaling](#automatic-efficient-marshaling)
49+
- [Stream across .NET and JS](#stream-across-net-and-js)
50+
- [Optional .NET native AOT compilation](#optional-net-native-aot-compilation)
51+
- [High performance](#high-performance)
52+
53+
### Load and call .NET assemblies from JS
54+
The `node-api-dotnet` package manages hosting the .NET runtime in the JS process
55+
(if not using AOT - see below). The .NET core library types are available directly on the
56+
`node-api-dotnet` module, and additional .NET assemblies can be loaded by file path:
2457
```JavaScript
58+
// JavaScript
2559
const dotnet = require('node-api-dotnet');
2660
const ExampleAssembly = dotnet.load('path/to/ExampleAssembly.dll');
2761
const exampleObj = new ExampleAssembly.ExampleClass(...args);
@@ -30,17 +64,56 @@ const exampleObj = new ExampleAssembly.ExampleClass(...args);
3064
.NET namespaces are stripped for convenience, but in case of ambiguity it's possible to get a type
3165
by full name:
3266
```JavaScript
67+
// JavaScript
3368
const MyType = ExampleAssembly['Namespace.Qualified.MyType'];
3469
```
3570

36-
### Generate type definitions for .NET APIs
71+
### Load and call JavaScript packages from .NET
72+
Calling JavaScript from .NET requires hosting a JS runtime such as Node.js in the .NET app.
73+
Then JS packages can be loaded either by directly invoking the JS `require()` function and
74+
working with low-level JS values, or by declaring C# interfaces for the JS types and using
75+
automatic marshalling.
76+
77+
All interaction with a JavaScript environment must be from its thread, via the
78+
`Run()`, `RunAsync()`, or `Post()` methods on the JS environment object.
79+
```C#
80+
// C#
81+
[JSImport("example-npm-package", "ExampleClass")]
82+
interface IExample
83+
{
84+
void ExampleMethod();
85+
}
86+
87+
var nodejsPlatform = new NodejsPlatform(libnodePath);
88+
var nodejs = nodejsPlatform.CreateEnvironment();
89+
90+
nodejs.Run(() => {
91+
// Use require() to load a module, then call a function on it.
92+
JSValue require = JSValue.Global["require"];
93+
var example1 = require.Call(default, "example-npm-package").GetProperty("ExampleClass");
94+
example1.CallMethod("exampleMethod");
95+
96+
// Call the same function using the imported interface.
97+
var example2 = nodejs.Import<IExample>();
98+
example2.ExampleMethod();
99+
});
100+
```
101+
102+
> Note: The `[JSImport]` attribute is in development. Until it is available, it is possible
103+
to create an interface adapter for a JS value with a little more code.
104+
105+
In the future, it may be possible to automatically generate .NET API definitions from TypeScript
106+
type definitions.
107+
108+
### Generate TS type definitions for .NET APIs
37109
If writing TypeScript, or type-checked JavaScript, there is a tool to generate type `.d.ts` type
38110
definitions for .NET APIs. Soon, it should also generate a small `.js` file that exports the
39111
assembly in a more natural way as a JS module.
40112
```bash
41113
$ npm exec node-api-dotnet-generator --assembly ExampleAssembly.dll --typedefs ExampleAssembly.d.ts
42114
```
43115
```TypeScript
116+
// TypeScript
44117
import { ExampleClass } from './ExampleAssembly';
45118
ExampleClass.ExampleMethod(...args); // This call is type-checked!
46119
```
@@ -52,6 +125,7 @@ JavaScript code can `await` a call to a .NET method that returns a `Task`. The m
52125
automatically sets up a `SynchronizationContext` so that the .NET result is returned back to the
53126
JS thread.
54127
```TypeScript
128+
// TypeScript
55129
import { ExampleClass } from './ExampleAssembly';
56130
const asyncResult = await ExampleClass.GetSomethingAsync(...args);
57131
```
@@ -72,6 +146,7 @@ part of the compilation and generates code to export the tagged APIs and marshal
72146
JavaScript and C#.
73147

74148
```C#
149+
// C#
75150
[JSExport] // Export class and all public members to JS.
76151
public class ExampleClass { ... }
77152

@@ -95,6 +170,7 @@ value of any type, and there are more types like `JSObject`, `JSArray`, `JSMap`,
95170
C# code can work directly with those types if desired:
96171

97172
```C#
173+
// C#
98174
[JSExport]
99175
public static JSPromise JSAsyncExample(JSValue input)
100176
{
@@ -148,10 +224,10 @@ transferred using shared memory (without any additional sockets or pipes), so me
148224
and copying is minimized.
149225

150226
### Optional .NET native AOT compilation
151-
This library supports hosting the .NET Runtime in the same process as the JavaScript engine.
227+
This library supports hosting the .NET Runtime in the same process as the JavaScript runtime.
152228
Alternatively, it also supports building [native ahead-of-time (AOT) compiled C#](
153-
https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) libraries that are
154-
loadable as a JavaScript module _without depending on the .NET Runtime_.
229+
https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) libraries that are
230+
loadable as a JavaScript module _without depending on the .NET Runtime_.
155231

156232
There are advantages and disadvantages to either approach:
157233
| | .NET Runtime | .NET Native AOT |
@@ -187,25 +263,26 @@ Thanks to these design choices, JS to .NET calls are [more than twice as fast](
187263
- .NET Framework 4.7.2 or later is supported at runtime,
188264
but .NET 6 SDK is still required for building.
189265
- Node.js v16 or later
190-
- Other JS engines may be supported in the future.
266+
- Other JS runtimes may be supported in the future.
191267
- OS: Windows, Mac, or Linux
192268
- It should work on any platform where .NET 6 is supported.
193269

194270
#### Instructions
195-
Choose between one of the following scenarios:
196-
- [Dynamically invoke .NET APIs from JavaScript](./Docs/dynamic-invoke.md)
197-
- [Develop a Node module in C#](./Docs/node-module.md)
198-
199-
Dynamic invocation is simpler to set up: all you need is the `node-api-dotnet` npm package and
200-
the path to a .NET assembly you want to call. But it has some limitations (not all kinds of APIs
201-
are supported), and is not quite as fast as a C# module, because marshalling code must be generated
202-
at runtime.
203-
204-
Alternatively, a C# Node module is appropriate for an application that has more advanced interop
205-
needs. It is faster because marshalling code can be generated at compile time, and the shape of
206-
the APIs exposed to JavaScript can be adapted with JS interop in mind.
207-
208-
TypeScript type definitions can be generated with either aproach.
271+
For calling .NET from JS, choose between one of the following scenarios:
272+
- [Dynamically invoke .NET APIs from JavaScript](./Docs/dynamic-invoke.md)<br/>
273+
Dynamic invocation is simpler to set up: all you need is the `node-api-dotnet` npm package and
274+
the path to a .NET assembly you want to call. But it has some limitations (not all kinds of APIs
275+
are supported), and is not quite as fast as a C# module, because marshalling code must be
276+
generated at runtime.
277+
- [Develop a Node module in C#](./Docs/node-module.md)<br/>
278+
A C# Node module is appropriate for an application that has more advanced interop needs. It can
279+
be faster because marshalling code can be generated at compile time, and the shape of the APIs
280+
exposed to JavaScript can be adapted with JS interop in mind.
281+
282+
For calling JS from .NET, more documentation will be added soon. For now, see the
283+
[`winui-fluid` example code](./examples/winui-fluid/).
284+
285+
Generated TypeScript type definitions can be utilized with any of these aproaches.
209286

210287
## Development
211288
For information about building, testing, and contributing changes to this project, see

examples/aot-module/aot-module.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
</PropertyGroup>
1111

1212
<ItemGroup>
13-
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.1.*-*" PrivateAssets="none" />
14-
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.1.*-*"
13+
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.2.*-*" PrivateAssets="none" />
14+
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.2.*-*"
1515
OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all" />
1616
</ItemGroup>
1717

examples/dotnet-module/dotnet-module.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.1.*-*" PrivateAssets="all" />
13-
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.1.*-*"
12+
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.2.*-*" PrivateAssets="all" />
13+
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.2.*-*"
1414
OutputItemType="Analyzer" ReferenceOutputAssembly="false" PrivateAssets="all" />
1515
</ItemGroup>
1616

examples/hermes-engine/hermes-engine.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</PropertyGroup>
1212

1313
<ItemGroup>
14-
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.1.*-*" PrivateAssets="all" />
14+
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.2.*-*" PrivateAssets="all" />
1515
<PackageReference Include="Microsoft.JavaScript.Hermes" Version="0.1.4" IncludeAssets="none" PrivateAssets="build;native;runtime" />
1616
</ItemGroup>
1717

examples/winui-fluid/App.xaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<Application
3+
x:Class="Microsoft.JavaScript.NodeApi.Examples.App"
4+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
5+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
6+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
7+
xmlns:local="using:Microsoft.JavaScript.NodeApi.Examples"
8+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
9+
mc:Ignorable="d">
10+
<Application.Resources>
11+
<ResourceDictionary>
12+
<ResourceDictionary.MergedDictionaries>
13+
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
14+
<!-- Other merged dictionaries here -->
15+
</ResourceDictionary.MergedDictionaries>
16+
<!-- Other app resources here -->
17+
<Color x:Key="Primary">#512BD4</Color>
18+
19+
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}" />
20+
<SolidColorBrush x:Key="WhiteBrush" Color="White" />
21+
<SolidColorBrush x:Key="BlackBrush" Color="Black" />
22+
23+
<x:Double x:Key="AppFontSize">14</x:Double>
24+
25+
<Style x:Key="MyLabel" TargetType="TextBlock">
26+
<Setter Property="Foreground"
27+
Value="{StaticResource PrimaryBrush}" />
28+
</Style>
29+
30+
<Style x:Key="Action" TargetType="Button">
31+
<Setter Property="FontSize"
32+
Value="{StaticResource AppFontSize}" />
33+
<Setter Property="Padding"
34+
Value="14,10" />
35+
</Style>
36+
37+
<Style x:Key="PrimaryAction"
38+
TargetType="Button"
39+
BasedOn="{StaticResource Action}">
40+
<Setter Property="Background"
41+
Value="{StaticResource PrimaryBrush}" />
42+
<Setter Property="CornerRadius"
43+
Value="8" />
44+
<Setter Property="Foreground"
45+
Value="{StaticResource WhiteBrush}" />
46+
</Style>
47+
</ResourceDictionary>
48+
</Application.Resources>
49+
</Application>

examples/winui-fluid/App.xaml.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.IO;
7+
using Microsoft.JavaScript.NodeApi.Runtimes;
8+
9+
namespace Microsoft.JavaScript.NodeApi.Examples;
10+
11+
/// <summary>
12+
/// Provides application-specific behavior to supplement the default Application class.
13+
/// </summary>
14+
public partial class App : Application
15+
{
16+
private Window? window;
17+
18+
/// <summary>
19+
/// Initializes the singleton application object. This is the first line of authored code
20+
/// executed, and as such is the logical equivalent of main() or WinMain().
21+
/// </summary>
22+
public App()
23+
{
24+
// Node.js require() searches for modules/packages relative to the CWD.
25+
Environment.CurrentDirectory = Path.GetDirectoryName(typeof(App).Assembly.Location)!;
26+
27+
string libnodePath = Path.Combine(
28+
Path.GetDirectoryName(typeof(App).Assembly.Location)!,
29+
"libnode.dll");
30+
NodejsPlatform nodejsPlatform = new(libnodePath);
31+
32+
Nodejs = nodejsPlatform.CreateEnvironment();
33+
if (Debugger.IsAttached)
34+
{
35+
int pid = Process.GetCurrentProcess().Id;
36+
Uri inspectionUri = Nodejs.StartInspector();
37+
Debug.WriteLine(
38+
$"Node.js ({pid}) inspector listening at {inspectionUri.AbsoluteUri}");
39+
}
40+
41+
this.InitializeComponent();
42+
}
43+
44+
/// <summary>
45+
/// Invoked when the application is launched normally by the end user. Other entry points
46+
/// will be used such as when the application is launched to open a specific file.
47+
/// </summary>
48+
/// <param name="args">Details about the launch request and process.</param>
49+
protected override void OnLaunched(LaunchActivatedEventArgs args)
50+
{
51+
window = new MainWindow();
52+
window.Activate();
53+
54+
window.Closed += OnMainWindowClosed;
55+
}
56+
57+
private void OnMainWindowClosed(object sender, WindowEventArgs args)
58+
{
59+
Nodejs.Dispose();
60+
}
61+
62+
public static new App Current => (App)Application.Current;
63+
64+
public NodejsEnvironment Nodejs { get; }
65+
}
432 Bytes
Loading
5.25 KB
Loading
1.71 KB
Loading

0 commit comments

Comments
 (0)