diff --git a/src/NodeApi.DotNetHost/JSMarshaller.cs b/src/NodeApi.DotNetHost/JSMarshaller.cs
index f9ba321e..185c600b 100644
--- a/src/NodeApi.DotNetHost/JSMarshaller.cs
+++ b/src/NodeApi.DotNetHost/JSMarshaller.cs
@@ -140,24 +140,6 @@ private string ToCamelCase(string name)
return sb.ToString();
}
- ///
- /// Converts a value to a JS value.
- ///
- public JSValue From(T value)
- {
- JSValue.From converter = GetToJSValueDelegate();
- return converter(value);
- }
-
- ///
- /// Converts a JS value to a requested type.
- ///
- public T To(JSValue value)
- {
- JSValue.To converter = GetFromJSValueDelegate();
- return converter(value);
- }
-
///
/// Checks whether a type is converted to a JavaScript built-in type.
///
@@ -2238,7 +2220,7 @@ private LambdaExpression BuildConvertToJSValueExpression(Type fromType)
{
statements = new[] { valueParameter };
}
- else if (fromType == typeof(object) || !fromType.IsPublic)
+ else if (fromType == typeof(object) || !(fromType.IsPublic || fromType.IsNestedPublic))
{
// Marshal unknown or nonpublic type as external, so at least it can be round-tripped.
Expression objectExpression = fromType.IsValueType ?
diff --git a/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs b/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs
index 2feb4f2f..16bf7067 100644
--- a/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs
+++ b/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs
@@ -15,42 +15,56 @@ public static class JSRuntimeContextExtensions
///
/// Imports a module or module property from JavaScript and converts it to an interface.
///
- /// Type of the value being imported.
+ /// .NET type that the imported JS value will be marshalled to.
/// Name of the module being imported, or null to import a
/// global property. This is equivalent to the value provided to import or
/// require() in JavaScript. Required if is null.
/// Name of a property on the module (or global), or null to import
/// the module object. Required if is null.
- /// The imported value.
+ /// JS marshaller instance to use to convert the imported value
+ /// to a .NET type.
+ /// The imported value, marshalled to the specified .NET type.
/// Both and
/// are null.
public static T Import(
this JSRuntimeContext runtimeContext,
string? module,
- string? property)
+ string? property,
+ bool esModule,
+ JSMarshaller marshaller)
{
- JSValue jsValue = runtimeContext.Import(module, property);
- return JSMarshaller.Current.To(jsValue);
+ if (marshaller == null) throw new ArgumentNullException(nameof(marshaller));
+
+ JSValue jsValue = runtimeContext.Import(module, property, esModule);
+ return marshaller.FromJS(jsValue);
}
///
/// Imports a module or module property from JavaScript and converts it to an interface.
///
- /// Type of the value being imported.
+ /// .NET type that the imported JS value will be marshalled to.
/// Name of the module being imported, or null to import a
/// global property. This is equivalent to the value provided to import or
/// require() in JavaScript. Required if is null.
/// Name of a property on the module (or global), or null to import
/// the module object. Required if is null.
- /// The imported value.
+ /// JS marshaller instance to use to convert the imported value
+ /// to a .NET type.
+ /// The imported value, marshalled to the specified .NET type.
/// Both and
/// are null.
public static T Import(
this NodejsEnvironment nodejs,
string? module,
- string? property)
+ string? property,
+ bool esModule,
+ JSMarshaller marshaller)
{
+ if (marshaller == null) throw new ArgumentNullException(nameof(marshaller));
+
JSValueScope scope = nodejs;
- return scope.RuntimeContext.Import(module, property);
+ return scope.RuntimeContext.Import(module, property, esModule, marshaller);
}
+
+ // TODO: ImportAsync()
}
diff --git a/src/NodeApi.DotNetHost/ManagedHost.cs b/src/NodeApi.DotNetHost/ManagedHost.cs
index 48c68705..4ac27471 100644
--- a/src/NodeApi.DotNetHost/ManagedHost.cs
+++ b/src/NodeApi.DotNetHost/ManagedHost.cs
@@ -52,15 +52,6 @@ public sealed class ManagedHost : JSEventEmitter, IDisposable
private JSValueScope? _rootScope;
- ///
- /// Strong reference to the JS object that is the exports for this module.
- ///
- ///
- /// The exports object has module APIs such as `require()` and `load()`, along with
- /// top-level .NET namespaces like `System` and `Microsoft`.
- ///
- private readonly JSReference _exports;
-
///
/// Component that dynamically exports types from loaded assemblies.
///
@@ -140,10 +131,8 @@ JSValue removeListener(JSCallbackArgs args)
AutoCamelCase = false,
};
- // Save the exports object, on which top-level namespaces will be defined.
- _exports = new JSReference(exports);
-
- _typeExporter = new()
+ // The type exporter will define top-level namespace properties on the exports object.
+ _typeExporter = new(JSMarshaller.Current, exports)
{
// Delay-loading is enabled by default, but can be disabled with this env variable.
IsDelayLoadEnabled =
@@ -151,13 +140,13 @@ JSValue removeListener(JSCallbackArgs args)
};
// Export the System.Runtime and System.Console assemblies by default.
- _typeExporter.ExportAssemblyTypes(typeof(object).Assembly, exports);
+ _typeExporter.ExportAssemblyTypes(typeof(object).Assembly);
_loadedAssembliesByName.Add(
typeof(object).Assembly.GetName().Name!, typeof(object).Assembly);
if (typeof(Console).Assembly != typeof(object).Assembly)
{
- _typeExporter.ExportAssemblyTypes(typeof(Console).Assembly, exports);
+ _typeExporter.ExportAssemblyTypes(typeof(Console).Assembly);
_loadedAssembliesByName.Add(
typeof(Console).Assembly.GetName().Name!, typeof(Console).Assembly);
}
@@ -222,11 +211,17 @@ public static napi_value InitializeModule(napi_env env, napi_value exports)
{
JSObject exportsObject = (JSObject)new JSValue(exports, scope);
- // Save the require() function that was passed in by the init script.
- JSValue require = exportsObject["require"];
- if (require.IsFunction())
+ // Save the require() and import() functions that were passed in by the init script.
+ JSValue requireFunction = exportsObject["require"];
+ if (requireFunction.IsFunction())
+ {
+ JSRuntimeContext.Current.RequireFunction = (JSFunction)requireFunction;
+ }
+
+ JSValue importFunction = exportsObject["import"];
+ if (importFunction.IsFunction())
{
- JSRuntimeContext.Current.Require = require;
+ JSRuntimeContext.Current.ImportFunction = (JSFunction)importFunction;
}
ManagedHost host = new(exportsObject)
@@ -513,7 +508,7 @@ private Assembly LoadAssembly(string assemblyNameOrFilePath, bool allowNativeLib
assembly = _loadContext.LoadFromAssemblyPath(assemblyFilePath);
#endif
- _typeExporter.ExportAssemblyTypes(assembly, (JSObject)_exports.GetValue()!.Value);
+ _typeExporter.ExportAssemblyTypes(assembly);
}
catch (BadImageFormatException)
{
diff --git a/src/NodeApi.DotNetHost/TypeExporter.cs b/src/NodeApi.DotNetHost/TypeExporter.cs
index f9228f50..78d0c75f 100644
--- a/src/NodeApi.DotNetHost/TypeExporter.cs
+++ b/src/NodeApi.DotNetHost/TypeExporter.cs
@@ -18,7 +18,19 @@ namespace Microsoft.JavaScript.NodeApi.DotNetHost;
///
/// Dynamically exports .NET types to JS.
///
-internal class TypeExporter
+///
+/// Exporting a .NET type:
+/// - Defines equivalent namespaced JS class prototype is defined, with a constructor function
+/// that calls back to the.NET constructor.
+/// - Defines static and instance properties and methods on the class prototype. Initially all
+/// of them are stubs (if is true (the default),
+/// but on first access each property gets redefined to call back to the corresponding .NET
+/// property or method. The callback uses marshalling code dynamically generated by a
+/// .
+/// - Registers a mapping between the .NET type and JS class/constructor object with the
+/// , for use in any marshalling operations.
+///
+public class TypeExporter
{
///
/// Mapping from top-level namespace names like `System` and `Microsoft` to
@@ -41,12 +53,24 @@ internal class TypeExporter
///
private readonly JSMarshaller _marshaller;
+ private readonly JSReference? _namespaces;
+
///
/// Creates a new instance of the class.
///
- public TypeExporter()
+ /// Used by the exporter to dynamically generate callback marshalling
+ /// code for exported members. Note the marshaller's
+ /// property controls the casing of members of exported types.
+ /// Optional JS object where top-level .NET namespace properties
+ /// (like "System") will be defined for exported types.
+ public TypeExporter(JSMarshaller marshaller, JSObject? namespaces = null)
{
- _marshaller = JSMarshaller.Current;
+ _marshaller = marshaller;
+
+ if (namespaces != null)
+ {
+ _namespaces = new JSReference(namespaces.Value);
+ }
}
///
@@ -55,9 +79,22 @@ public TypeExporter()
///
public bool IsDelayLoadEnabled { get; set; } = true;
- public void ExportAssemblyTypes(Assembly assembly, JSObject exports)
+ ///
+ /// Exports all types from a .NET assembly to JavaScript.
+ ///
+ ///
+ /// If a JS "namespaces" object was passed to the constructor,
+ /// this method may register additional top-level namespaces on that object for types in the
+ /// assembly.
+ ///
+ /// If is true (the default), then individual types in the
+ /// assembly are not fully exported until they are referenced either directly or by a
+ /// dependency.
+ ///
+ public void ExportAssemblyTypes(Assembly assembly)
{
- Trace($"> ManagedHost.LoadAssemblyTypes({assembly.GetName().Name})");
+ string assemblyName = assembly.GetName().Name!;
+ Trace($"> {nameof(TypeExporter)}.ExportAssemblyTypes({assemblyName})");
int count = 0;
List typeProxies = new();
@@ -83,8 +120,14 @@ public void ExportAssemblyTypes(Assembly assembly, JSObject exports)
{
// Export a new top-level namespace.
parentNamespace = new NamespaceProxy(namespaceParts[0], null, this);
- exports[namespaceParts[0]] = parentNamespace.Value;
_exportedNamespaces.Add(namespaceParts[0], parentNamespace);
+
+ if (_namespaces != null)
+ {
+ // Add a property on the namespaces JS object.
+ JSObject namespacesObject = (JSObject)_namespaces.GetValue()!.Value;
+ namespacesObject[namespaceParts[0]] = parentNamespace.Value;
+ }
}
for (int i = 1; i < namespaceParts.Length; i++)
@@ -147,7 +190,7 @@ public void ExportAssemblyTypes(Assembly assembly, JSObject exports)
ExportExtensionMethod(extensionMethod);
}
- Trace($"< ManagedHost.LoadAssemblyTypes({assembly.GetName().Name}) => {count} types");
+ Trace($"< {nameof(TypeExporter)}.ExportAssemblyTypes({assemblyName}) => {count} types");
}
private void RegisterDerivedType(TypeProxy derivedType, Type? baseOrInterfaceType = null)
@@ -265,7 +308,7 @@ private static bool IsExtensionTargetTypeSupported(Type targetType, string exten
return true;
}
- public NamespaceProxy? GetNamespaceProxy(string ns)
+ internal NamespaceProxy? GetNamespaceProxy(string ns)
{
string[] namespaceParts = ns.Split('.');
if (!_exportedNamespaces.TryGetValue(
@@ -287,7 +330,7 @@ private static bool IsExtensionTargetTypeSupported(Type targetType, string exten
return namespaceProxy;
}
- public TypeProxy? GetTypeProxy(Type type)
+ internal TypeProxy? GetTypeProxy(Type type)
{
if (type.IsConstructedGenericType)
{
@@ -314,11 +357,11 @@ private static bool IsExtensionTargetTypeSupported(Type targetType, string exten
/// never actually used. The default is from .
/// A strong reference to a JS object that represents the exported type, or null
/// if the type could not be exported.
- public JSReference? TryExportType(Type type, bool? deferMembers = null)
+ internal JSReference? TryExportType(Type type, bool? deferMembers = null)
{
try
{
- return ExportType(type, deferMembers ?? IsDelayLoadEnabled);
+ return ExportType(type, deferMembers);
}
catch (NotSupportedException ex)
{
@@ -332,7 +375,24 @@ private static bool IsExtensionTargetTypeSupported(Type targetType, string exten
}
}
- private JSReference ExportType(Type type, bool deferMembers)
+ ///
+ /// Exports a specific .NET type to JS.
+ ///
+ /// The .NET type to export.
+ /// True to delay exporting of all type members until each one is
+ /// accessed. If false, all type members are immediately exported, which may cascade to
+ /// exporting many additional types referenced by the members, including members that are
+ /// never actually used. The default is from .
+ /// A strong reference to a JS object that represents the exported type.
+ /// The .NET type cannot be exported.
+ ///
+ /// This method does NOT register namespaces for the exported type on the JS "namespaces"
+ /// object (if one was passed to the constructor). It is
+ /// sufficient for explicit marshalling of the exported type using .NET code, but not
+ /// for dynamic access of the .NET type from JS code. Use
+ /// instead for full namespace export.
+ ///
+ public JSReference ExportType(Type type, bool? deferMembers = null)
{
if (!IsSupportedType(type))
{
@@ -358,7 +418,7 @@ private JSReference ExportType(Type type, bool deferMembers)
}
else
{
- return ExportClass(type, deferMembers);
+ return ExportClass(type, deferMembers ?? IsDelayLoadEnabled);
}
}
else
@@ -537,9 +597,15 @@ private void ExportTypeIfSupported(Type dependencyType, bool deferMembers)
#endif
IsSupportedType(dependencyType))
{
- TypeProxy typeProxy = GetTypeProxy(dependencyType) ??
- throw new InvalidOperationException(
- $"Type proxy not found for dependency: {dependencyType.FormatName()}");
+ TypeProxy? typeProxy = GetTypeProxy(dependencyType);
+ if (typeProxy == null)
+ {
+ ExportAssemblyTypes(dependencyType.Assembly);
+ typeProxy = GetTypeProxy(dependencyType) ??
+ throw new InvalidOperationException(
+ $"Dependency type was not exported: {dependencyType.FormatName()}");
+ }
+
typeProxy.Export();
}
}
diff --git a/src/NodeApi/DotNetHost/NativeHost.cs b/src/NodeApi/DotNetHost/NativeHost.cs
index eb0790df..8431e8f5 100644
--- a/src/NodeApi/DotNetHost/NativeHost.cs
+++ b/src/NodeApi/DotNetHost/NativeHost.cs
@@ -116,6 +116,7 @@ private JSValue InitializeManagedHost(JSCallbackArgs args)
}
JSValue require = args[2];
+ JSValue import = args[3];
Trace($"> NativeHost.InitializeManagedHost({targetFramework}, {managedHostPath})");
try
@@ -130,7 +131,8 @@ private JSValue InitializeManagedHost(JSCallbackArgs args)
int.Parse(targetFramework.Substring(4, 1)),
targetFramework.Length == 5 ? 0 :
int.Parse(targetFramework.Substring(5, 1)));
- exports = InitializeFrameworkHost(frameworkVersion, managedHostPath, require);
+ exports = InitializeFrameworkHost(
+ frameworkVersion, managedHostPath, require, import);
}
else
{
@@ -140,7 +142,8 @@ private JSValue InitializeManagedHost(JSCallbackArgs args)
#else
Version dotnetVersion = Version.Parse(targetFramework.AsSpan(3));
#endif
- exports = InitializeDotNetHost(dotnetVersion, managedHostPath, require);
+ exports = InitializeDotNetHost(
+ dotnetVersion, managedHostPath, require, import);
}
// Save init parameters and result in case of re-init.
@@ -166,11 +169,13 @@ private JSValue InitializeManagedHost(JSCallbackArgs args)
/// Minimum requested .NET version.
/// Path to the managed host assembly file.
/// Require function passed in by the init script.
+ /// Import function passed in by the init script.
/// JS exports value from the managed host.
private JSValue InitializeFrameworkHost(
Version minVersion,
string managedHostPath,
- JSValue require)
+ JSValue require,
+ JSValue import)
{
Trace(" Initializing .NET Framework " + minVersion);
@@ -196,6 +201,7 @@ private JSValue InitializeFrameworkHost(
// Create an "exports" object for the managed host module initialization.
JSValue exportsValue = JSValue.CreateObject();
exportsValue.SetProperty("require", require);
+ exportsValue.SetProperty("import", import);
napi_env env = (napi_env)exportsValue.Scope;
napi_value exports = (napi_value)exportsValue;
@@ -235,11 +241,13 @@ private JSValue InitializeFrameworkHost(
/// Requested .NET version.
/// Path to the managed host assembly file.
/// Require function passed in by the init script.
+ /// Import function passed in by the init script.
/// JS exports value from the managed host.
private JSValue InitializeDotNetHost(
Version targetVersion,
string managedHostPath,
- JSValue require)
+ JSValue require,
+ JSValue import)
{
Trace(" Initializing .NET " + targetVersion);
@@ -304,6 +312,7 @@ private JSValue InitializeDotNetHost(
// Create an "exports" object for the managed host module initialization.
var exports = JSValue.CreateObject();
exports.SetProperty("require", require);
+ exports.SetProperty("import", import);
// Define a dispose method implemented by the native host that closes the CLR context.
// The managed host proxy will pass through dispose calls to this callback.
diff --git a/src/NodeApi/Interop/EmptyAttributes.cs b/src/NodeApi/Interop/EmptyAttributes.cs
index 0ebe9189..31eaaf14 100644
--- a/src/NodeApi/Interop/EmptyAttributes.cs
+++ b/src/NodeApi/Interop/EmptyAttributes.cs
@@ -65,4 +65,13 @@ public CallerArgumentExpressionAttribute(string parameterName)
}
}
+namespace System.Diagnostics
+{
+ [AttributeUsage(AttributeTargets.Method, Inherited = false)]
+ public sealed class StackTraceHiddenAttribute : Attribute
+ {
+ public StackTraceHiddenAttribute() {}
+ }
+}
+
#endif
diff --git a/src/NodeApi/Interop/JSRuntimeContext.cs b/src/NodeApi/Interop/JSRuntimeContext.cs
index 92bf11dc..1e64f953 100644
--- a/src/NodeApi/Interop/JSRuntimeContext.cs
+++ b/src/NodeApi/Interop/JSRuntimeContext.cs
@@ -8,6 +8,7 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
+using System.Threading.Tasks;
using Microsoft.JavaScript.NodeApi.Runtime;
using static Microsoft.JavaScript.NodeApi.Interop.JSCollectionProxies;
using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime;
@@ -99,9 +100,14 @@ private readonly ConcurrentDictionary