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

WindowsDesktop targeting pack, WPF example project #162

Merged
merged 2 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Docs/dynamic-invoke.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ For examples of this scenario, see
.NET system assemblies can also be loaded and used. For example, import `System.Runtime.js` to
get the core types, `System.Console.js` to get console APIs, etc. Type definitions for those
two assembiles are generated by default; to generate typedefs for additional system assemblies,
add items to the `NodeApiSystemReferenceAssemblies` MSBuild item-list in the project.
add items to the `NodeApiSystemReferenceAssembly` MSBuild item-list in the project.

> :warning: Generic types and methods are not yet supported very well -- with the exception of
generic collections which work great.
7 changes: 7 additions & 0 deletions examples/wpf/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/node_modules
/pkg
/obj
/bin

*.d.ts
package-lock.json
12 changes: 12 additions & 0 deletions examples/wpf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

## Example: Calling WFP APIs from JS
The `example.js` script dynamically loads the WPF .NET assemblies and shows a simple message box.

_**.NET events** are not yet projected to JS ([#59](https://github.com/microsoft/node-api-dotnet/issues/59)). WPF capabilities will be limited until that issue is resolved._

| Command | Explanation
|----------------------------------|--------------------------------------------------
| `dotnet pack ../..` | Build Node API .NET packages.
| `dotnet build` | Generate type definitions for WPF assemblies.
| `npm install` | Install `node-api-dotnet` npm package into the project.
| `node example.js` | Run example JS code that calls WPF.
9 changes: 9 additions & 0 deletions examples/wpf/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// @ts-check

import dotnet from 'node-api-dotnet';
import './bin/PresentationFramework.js';

dotnet.System.Windows.MessageBox.Show('Hello from JS!', "Example");
7 changes: 7 additions & 0 deletions examples/wpf/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "node-api-dotnet-examples-semantic-kernel",
"type": "module",
"dependencies": {
"node-api-dotnet": "file:../../out/pkg/node-api-dotnet"
}
}
26 changes: 26 additions & 0 deletions examples/wpf/wpf.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
<RestorePackagesPath>$(MSBuildThisFileDirectory)/pkg</RestorePackagesPath>
<OutDir>bin</OutDir>
<NodeApiAssemblyJSModuleType>esm</NodeApiAssemblyJSModuleType>
<UseWPF>true</UseWPF>
</PropertyGroup>

<ItemGroup>
<NodeApiSystemReferenceAssembly Include="WindowsBase" />
<NodeApiSystemReferenceAssembly Include="PresentationCore" />
<NodeApiSystemReferenceAssembly Include="PresentationFramework" />
<NodeApiSystemReferenceAssembly Include="UIAutomationTypes" />
<NodeApiSystemReferenceAssembly Include="System.IO.Packaging" />
<NodeApiSystemReferenceAssembly Include="System.Security.Cryptography.Xml" />
<NodeApiSystemReferenceAssembly Include="System.Windows" />
<NodeApiSystemReferenceAssembly Include="System.Windows.Extensions" />
<NodeApiSystemReferenceAssembly Include="System.Windows.Input.Manipulations" />
<NodeApiSystemReferenceAssembly Include="System.Xaml" />
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.4.*-*" />
</ItemGroup>

</Project>
11 changes: 11 additions & 0 deletions src/NodeApi.DotNetHost/ManagedHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,17 @@ private Assembly LoadAssembly(string assemblyNameOrFilePath)
assemblyFilePath = Path.Combine(
Path.GetDirectoryName(typeof(object).Assembly.Location)!,
assemblyFilePath + ".dll");

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Also support loading Windows-specific system assemblies.
string assemblyFilePath2 = assemblyFilePath.Replace(
"Microsoft.NETCore.App", "Microsoft.WindowsDesktop.App");
if (File.Exists(assemblyFilePath2))
{
assemblyFilePath = assemblyFilePath2;
}
}
}
else if (!Path.IsPathRooted(assemblyFilePath))
{
Expand Down
16 changes: 8 additions & 8 deletions src/NodeApi.Generator/NodeApi.Generator.targets
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
Outputs="$(TargetDir)$(NodeApiTypeDefinitionsFileName)"
Condition=" '$(GenerateNodeApiTypeDefinitions)' == 'true' AND Exists('$(TargetPath)') "
>
<Exec Command="dotnet &quot;$(NodeApiGeneratorAssemblyPath)&quot; --assembly &quot;$(TargetPath)&quot; --reference &quot;@(ReferencePathWithRefAssemblies)&quot; --typedefs &quot;$(TargetDir)$(NodeApiTypeDefinitionsFileName)&quot; $(NodeApiTypeDefinitionsGeneratorOptions)"
<Exec Command="dotnet &quot;$(NodeApiGeneratorAssemblyPath)&quot; --assembly &quot;$(TargetPath)&quot; --packs &quot;@(TargetingPack)&quot; --reference &quot;@(ReferencePathWithRefAssemblies)&quot; --typedefs &quot;$(TargetDir)$(NodeApiTypeDefinitionsFileName)&quot; $(NodeApiTypeDefinitionsGeneratorOptions)"
ConsoleToMSBuild="true" />
</Target>

Expand Down Expand Up @@ -85,23 +85,23 @@
</PropertyGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('net4'))">
<NodeApiSystemReferenceAssemblies Include="mscorlib" />
<NodeApiSystemReferenceAssemblies Include="System" />
<NodeApiSystemReferenceAssembly Include="mscorlib" />
<NodeApiSystemReferenceAssembly Include="System" />
</ItemGroup>
<ItemGroup Condition="! $(TargetFramework.StartsWith('net4'))">
<NodeApiSystemReferenceAssemblies Include="System.Runtime" />
<NodeApiSystemReferenceAssemblies Include="System.Console" />
<NodeApiSystemReferenceAssembly Include="System.Runtime" />
<NodeApiSystemReferenceAssembly Include="System.Console" />
</ItemGroup>
<ItemGroup>
<!-- This does not use @(NodeApiReferenceAssemblies), like the target Inputs, to avoid excluding items that are up-to-date. -->
<_NodeApiAllReferenceAssemblies Include="@(RuntimeCopyLocalItems)" />
<_NodeApiAllReferenceAssemblies Include="@(NodeApiSystemReferenceAssemblies)" />
<_NodeApiAllReferenceAssemblies Include="@(NodeApiSystemReferenceAssembly)" />

<_NodeApiAllTypeDefs Include="@(RuntimeCopyLocalItems->'$(TargetDir)%(Filename).d.ts')" />
<_NodeApiAllTypeDefs Include="@(NodeApiSystemReferenceAssemblies->'$(TargetDir)%(Identity).d.ts')" />
<_NodeApiAllTypeDefs Include="@(NodeApiSystemReferenceAssembly->'$(TargetDir)%(Identity).d.ts')" />
</ItemGroup>

<Exec Command="dotnet &quot;$(NodeApiGeneratorAssemblyPath)&quot; $(_SuppressTSGenerationWarnings) --assemblies &quot;@(_NodeApiAllReferenceAssemblies)&quot; --typedefs &quot;@(_NodeApiAllTypeDefs)&quot; $(NodeApiTypeDefinitionsGeneratorOptions)"
<Exec Command="dotnet &quot;$(NodeApiGeneratorAssemblyPath)&quot; $(_SuppressTSGenerationWarnings) --assemblies &quot;@(_NodeApiAllReferenceAssemblies)&quot; --packs &quot;@(TargetingPack)&quot; --typedefs &quot;@(_NodeApiAllTypeDefs)&quot; $(NodeApiTypeDefinitionsGeneratorOptions)"
ConsoleToMSBuild="true" />
</Target>

Expand Down
121 changes: 92 additions & 29 deletions src/NodeApi.Generator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ public static class Program
private const char PathSeparator = ';';

private static readonly List<string> s_assemblyPaths = new();
private static readonly List<string> s_referenceAssemblyDirectories = new();
private static readonly List<string> s_referenceAssemblyPaths = new();
private static readonly List<string> s_typeDefinitionsPaths = new();
private static readonly HashSet<int> s_systemAssemblyIndexes = new();
private static string? s_systemAssemblyDirectory;
private static TypeDefinitionsGenerator.ModuleType s_moduleType;
private static bool s_suppressWarnings;

Expand All @@ -49,6 +49,7 @@ public static int Main(string[] args)
Console.WriteLine(" -a --asssembly Path to input assembly (required)");
Console.WriteLine(" -f --framework Target framework of system assemblies " +
"(optional)");
Console.WriteLine(" -p --pack Targeting pack (optional, multiple)");
Console.WriteLine(" -r --reference Path to reference assembly " +
"(optional, multiple)");
Console.WriteLine(" -t --typedefs Path to output type definitions file (required)");
Expand All @@ -70,10 +71,10 @@ public static int Main(string[] args)
TypeDefinitionsGenerator.GenerateTypeDefinitions(
s_assemblyPaths[i],
allReferencePaths,
s_referenceAssemblyDirectories,
s_typeDefinitionsPaths[i],
s_moduleType,
isSystemAssembly: s_systemAssemblyIndexes.Contains(i),
s_systemAssemblyDirectory,
s_suppressWarnings);

if (s_moduleType != TypeDefinitionsGenerator.ModuleType.None)
Expand All @@ -91,6 +92,7 @@ public static int Main(string[] args)
private static bool ParseArgs(string[] args)
{
string? targetFramework = null;
List<string> targetingPacks = new();

for (int i = 0; i < args.Length; i++)
{
Expand Down Expand Up @@ -122,6 +124,12 @@ void AddItems(List<string> list, string items)
targetFramework = args[++i];
break;

case "-p":
case "--pack":
case "--packs":
AddItems(targetingPacks, args[++i]);
break;

case "-r":
case "--reference":
case "--references":
Expand Down Expand Up @@ -163,21 +171,31 @@ void AddItems(List<string> list, string items)
}
}

ResolveSystemAssemblies(targetFramework);
ResolveSystemAssemblies(targetFramework, targetingPacks);

bool HasAssemblyExtension(string fileName) =>
fileName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
fileName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase);

if (s_assemblyPaths.Any((a) => !HasAssemblyExtension(a)) ||
s_referenceAssemblyPaths.Any((r) => !HasAssemblyExtension(r)) ||
s_typeDefinitionsPaths.Any(
(t) => !t.EndsWith(".d.ts", StringComparison.OrdinalIgnoreCase)))
string? invalidAssemblyPath = s_assemblyPaths.Concat(s_referenceAssemblyPaths)
.FirstOrDefault((a) => !HasAssemblyExtension(a));
if (invalidAssemblyPath != null)
{
Console.WriteLine("Incorrect file path or extension.");
Console.WriteLine(
"Incorrect assembly file extension: " + Path.GetFileName(invalidAssemblyPath));
return false;
}
else if (s_assemblyPaths.Count == 0)

string? invalidTypedefPath = s_typeDefinitionsPaths.FirstOrDefault(
(t) => !t.EndsWith(".d.ts", StringComparison.OrdinalIgnoreCase));
if (invalidTypedefPath != null)
{
Console.WriteLine(
"Incorrect typedef file extension: " + Path.GetFileName(invalidTypedefPath));
return false;
}

if (s_assemblyPaths.Count == 0)
{
Console.WriteLine("Specify an assembly file path.");
return false;
Expand All @@ -196,12 +214,27 @@ bool HasAssemblyExtension(string fileName) =>
return true;
}

private static void ResolveSystemAssemblies(string? targetFramework)
private static void ResolveSystemAssemblies(
string? targetFramework,
List<string> targetingPacks)
{
targetFramework ??= GetCurrentFrameworkTarget();
if (targetFramework == null)
{
targetFramework = GetCurrentFrameworkTarget();
}
else if (targetFramework.Contains('-'))
{
// Strip off a platform suffix from a target framework like "net6.0-windows".
targetFramework = targetFramework.Substring(0, targetFramework.IndexOf('-'));
}

if (targetFramework.StartsWith("net4"))
{
if (targetingPacks.Count > 0)
{
Console.WriteLine("Ignoring target packs for .NET Framework target");
}

string refAssemblyDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
"Reference Assemblies",
Expand All @@ -211,11 +244,17 @@ private static void ResolveSystemAssemblies(string? targetFramework)
"v" + string.Join(".", targetFramework.Substring(3).ToArray())); // v4.7.2
if (Directory.Exists(refAssemblyDirectory))
{
s_systemAssemblyDirectory = refAssemblyDirectory;
s_referenceAssemblyDirectories.Add(refAssemblyDirectory);
}
}
else
{
if (targetingPacks.Count == 0)
{
// If no targeting packs were specified, use the deafult targeting pack for .NET.
targetingPacks.Add("Microsoft.NETCore.App");
}

string runtimeDirectory = RuntimeEnvironment.GetRuntimeDirectory();
if (runtimeDirectory[runtimeDirectory.Length - 1] == Path.DirectorySeparatorChar)
{
Expand All @@ -225,36 +264,60 @@ private static void ResolveSystemAssemblies(string? targetFramework)
string dotnetRootDirectory = Path.GetDirectoryName(Path.GetDirectoryName(
Path.GetDirectoryName(runtimeDirectory)!)!)!;

string refAssemblyDirectory = Path.Combine(
dotnetRootDirectory,
"packs",
"Microsoft.NETCore.App.Ref");
s_systemAssemblyDirectory = Directory.GetDirectories(refAssemblyDirectory)
.OrderByDescending((d) => Path.GetFileName(d))
.Select((d) => Path.Combine(d, "ref", targetFramework))
.FirstOrDefault(Directory.Exists);
}
foreach (string targetPack in targetingPacks)
{
string targetPackDirectory = Path.Combine(
dotnetRootDirectory,
"packs",
targetPack + ".Ref");
if (Directory.Exists(targetPackDirectory))
{
string? refAssemblyDirectory = Directory.GetDirectories(targetPackDirectory)
.OrderByDescending((d) => Path.GetFileName(d))
.Select((d) => Path.Combine(d, "ref", targetFramework))
.FirstOrDefault(Directory.Exists);
if (refAssemblyDirectory != null)
{
s_referenceAssemblyDirectories.Add(refAssemblyDirectory);
}
}
}

if (s_systemAssemblyDirectory == null)
{
// TODO: Check .NET Framework.
return;
// Reverse the order of reference assembly directories so that reference assemblies
// specified later in the list are searched first (they override earlier directories).
s_referenceAssemblyDirectories.Reverse();
}

for (int i = 0; i < s_assemblyPaths.Count; i++)
{
if (!s_assemblyPaths[i].EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
{
string systemAssemblyPath = Path.Combine(
s_systemAssemblyDirectory, s_assemblyPaths[i] + ".dll");
if (File.Exists(systemAssemblyPath))
string? systemAssemblyPath = null;
foreach (string referenceAssemblyDirectory in s_referenceAssemblyDirectories)
{
string potentialSystemAssemblyPath = Path.Combine(
referenceAssemblyDirectory, s_assemblyPaths[i] + ".dll");
if (File.Exists(potentialSystemAssemblyPath))
{
systemAssemblyPath = potentialSystemAssemblyPath;
break;
}
}

if (systemAssemblyPath != null)
{
s_assemblyPaths[i] = systemAssemblyPath;
s_systemAssemblyIndexes.Add(i);
}
else
{
Console.WriteLine("System assembly not found at " + systemAssemblyPath);
Console.WriteLine(
$"Assembly '{s_assemblyPaths[i]}' was not found in " +
"reference assembly directories:");
foreach (string referenceAssemblyDirectory in s_referenceAssemblyDirectories)
{
Console.WriteLine(" " + referenceAssemblyDirectory);
}
}
}
}
Expand Down
16 changes: 9 additions & 7 deletions src/NodeApi.Generator/TypeDefinitionsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,24 +128,26 @@ public enum ModuleType
public static void GenerateTypeDefinitions(
string assemblyPath,
IEnumerable<string> referenceAssemblyPaths,
IEnumerable<string> systemReferenceAssemblyDirectories,
string typeDefinitionsPath,
ModuleType loaderModuleType,
bool isSystemAssembly = false,
string? systemReferenceAssemblyDirectory = null,
bool suppressWarnings = false)
{
// Create a metadata load context that includes a resolver for .NET system assemblies
// along with the target assembly.

systemReferenceAssemblyDirectory ??= RuntimeEnvironment.GetRuntimeDirectory();
string[] systemAssemblies = Directory.GetFiles(
systemReferenceAssemblyDirectory, "*.dll");
// Resolve all assemblies in all the system reference assembly directories.
string[] systemAssemblies = systemReferenceAssemblyDirectories
.SelectMany((d) => Directory.GetFiles(d, "*.dll"))
.ToArray();

// Drop reference assemblies that are already in the system directory.
// Drop reference assemblies that are already in any system ref assembly directories.
// (They would only support older framework versions.)
referenceAssemblyPaths = referenceAssemblyPaths.Where(
(r) => !systemAssemblies.Any((a) =>
Path.GetFileName(a).Equals(Path.GetFileName(r), StringComparison.OrdinalIgnoreCase)));
(r) => Path.GetFileNameWithoutExtension(r).Equals("WindowsBase") ||
!systemAssemblies.Any((a) => Path.GetFileName(a).Equals(
Path.GetFileName(r), StringComparison.OrdinalIgnoreCase)));

PathAssemblyResolver assemblyResolver = new(
new[] { typeof(object).Assembly.Location }
Expand Down