From 9cf91cb31811e17c675435c5add07539ee590d41 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Mon, 1 May 2023 19:22:32 +0300 Subject: [PATCH 1/2] Implement a ShadowRealm API see https://github.com/tc39/proposal-shadowrealm --- NiL.JS/BaseLibrary/ShadowRealm.cs | 43 +++++++++ NiL.JS/Core/Context.cs | 3 + NiL.JS/Extensions/ContextExtensions.cs | 39 ++++++++ NiL.JS/Extensions/JSValueExtensions.cs | 5 +- NiL.JS/NiL.JS.csproj | 4 +- Tests/ShadowRealmTest.cs | 120 +++++++++++++++++++++++++ 6 files changed, 211 insertions(+), 3 deletions(-) create mode 100644 NiL.JS/BaseLibrary/ShadowRealm.cs create mode 100644 Tests/ShadowRealmTest.cs diff --git a/NiL.JS/BaseLibrary/ShadowRealm.cs b/NiL.JS/BaseLibrary/ShadowRealm.cs new file mode 100644 index 000000000..4df28d3ef --- /dev/null +++ b/NiL.JS/BaseLibrary/ShadowRealm.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using NiL.JS.Core; +using NiL.JS.Core.Interop; +using NiL.JS.Extensions; + +namespace NiL.JS.BaseLibrary; + + +[RequireNewKeyword] + +public sealed class ShadowRealm +{ + private readonly Module _mod; + + internal ShadowRealm(IModuleResolver[] allowedModules) + { + _mod = new Module("", ""); + _mod.Context.DefineVariable("globalThis").Assign(_mod.Context.ThisBind); + foreach (var moduleResolver in allowedModules) + { + _mod.ModuleResolversChain.Add(moduleResolver); + } + } + + + + public JSValue evaluate(Arguments a) + { + var str = a[0].As(); + return _mod.Context.Eval(str); + + } + + public JSValue importValue(Arguments a) + { + var path = a[0].As(); + var name = a[1].As(); + var imp = _mod.Import(path); + var promise = + new Promise(Task.FromResult(name == "default" ? imp.Exports.Default : imp.Exports[name])); + return _mod.Context.GlobalContext.ProxyValue(promise); + } +} \ No newline at end of file diff --git a/NiL.JS/Core/Context.cs b/NiL.JS/Core/Context.cs index b4c737080..9d624aeb8 100644 --- a/NiL.JS/Core/Context.cs +++ b/NiL.JS/Core/Context.cs @@ -7,6 +7,7 @@ using System.Threading; using NiL.JS.BaseLibrary; using NiL.JS.Core.Functions; +using NiL.JS.Extensions; using NiL.JS.Statements; #if NET40 @@ -195,6 +196,7 @@ public Context() public Context(Context prototype) : this(prototype, true, Function.Empty) { + } public Context(Context prototype, bool strict) @@ -207,6 +209,7 @@ public Context(Context prototype, bool strict) public Context(bool strict) : this(CurrentGlobalContext, strict) { + } internal Context(Context prototype, bool createFields, Function owner) diff --git a/NiL.JS/Extensions/ContextExtensions.cs b/NiL.JS/Extensions/ContextExtensions.cs index 0d416357d..782321e39 100644 --- a/NiL.JS/Extensions/ContextExtensions.cs +++ b/NiL.JS/Extensions/ContextExtensions.cs @@ -1,5 +1,8 @@ using System; +using NiL.JS.BaseLibrary; using NiL.JS.Core; +using NiL.JS.Core.Functions; +using Array = NiL.JS.BaseLibrary.Array; namespace NiL.JS.Extensions { @@ -10,6 +13,42 @@ public static void Add(this Context context, string key, object value) context.DefineVariable(key).Assign(context.GlobalContext.ProxyValue(value)); } + + /// + /// Add implementation of ShadowRealm API + /// + /// + /// + /// that used for importValue + /// At current moment not allowed to use Shadow Realm in Shadow realm + /// Context with ShadowRealm contructor + public static Context AddShadowRealm(this Context context, IModuleResolver[] allowedResolvers = null) + { + //Workaround + context.DefineVariable("globalThis").Assign(context.ThisBind); + var resolvers = allowedResolvers ?? System.Array.Empty(); + var del = new Func(() => new ShadowRealm(resolvers)); + var func = context.GlobalContext.ProxyValue(del).As(); + func.RequireNewKeywordLevel = RequireNewKeywordLevel.WithNewOnly; + context.DefineVariable("ShadowRealm").Assign(func); + return context; + } + + /// + /// Add implementation of ShadowRealm API to module context + /// + /// + /// + /// that used for importValue. If null - used from ModuleResolversChain + /// At current moment not allowed to use Shadow Realm in Shadow realm + /// Context with ShadowRealm contructor + public static Module AddShadowRealm(this Module module, IModuleResolver[] allowedResolvers = null) + { + AddShadowRealm(module.Context, allowedResolvers ?? module.ModuleResolversChain.ToArray()); + return module; + } + + public static void Add(this Context context, string key, JSValue value) { context.DefineVariable(key).Assign(value); diff --git a/NiL.JS/Extensions/JSValueExtensions.cs b/NiL.JS/Extensions/JSValueExtensions.cs index c54a19be4..de5606a3f 100644 --- a/NiL.JS/Extensions/JSValueExtensions.cs +++ b/NiL.JS/Extensions/JSValueExtensions.cs @@ -5,6 +5,7 @@ using System.Reflection.Emit; using NiL.JS.BaseLibrary; using NiL.JS.Core; +using NiL.JS.Core.Functions; using NiL.JS.Core.Interop; namespace NiL.JS.Extensions @@ -106,9 +107,11 @@ public static T As(this JSValue self) case TypeCode.Double: return GetDefinedOr(self, (T)(object)double.NaN); } - + return GetDefinedOr(self, default(T)); } + + public static T GetDefinedOr(this JSValue self, T defaultValue) { diff --git a/NiL.JS/NiL.JS.csproj b/NiL.JS/NiL.JS.csproj index 63906a965..9cac40d14 100644 --- a/NiL.JS/NiL.JS.csproj +++ b/NiL.JS/NiL.JS.csproj @@ -33,8 +33,8 @@ - - + + diff --git a/Tests/ShadowRealmTest.cs b/Tests/ShadowRealmTest.cs new file mode 100644 index 000000000..671c83101 --- /dev/null +++ b/Tests/ShadowRealmTest.cs @@ -0,0 +1,120 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NiL.JS; +using NiL.JS.BaseLibrary; +using NiL.JS.Core; +using NiL.JS.Extensions; + +namespace Tests; + +[TestClass] +public class ShadowRealmTest +{ + private sealed class DelegateModuleResolver : IModuleResolver + { + private readonly ModuleResolverDelegate _moduleResolverDelegate; + + public delegate bool ModuleResolverDelegate(ModuleRequest moduleRequest, out Module result); + + public DelegateModuleResolver(ModuleResolverDelegate moduleResolverDelegate) + { + _moduleResolverDelegate = moduleResolverDelegate ?? + throw new ArgumentNullException(nameof(moduleResolverDelegate)); + } + + public bool TryGetModule(ModuleRequest moduleRequest, out Module result) + { + return _moduleResolverDelegate(moduleRequest, out result); + } + } + + + [TestMethod] + public void GlobalThisExist() + { + var ctx = new Context().AddShadowRealm(); + ctx.DefineVariable("lol").Assign(123); + Assert.IsTrue(ctx.Eval("globalThis === this").As()); + Assert.IsTrue(ctx.Eval("globalThis.lol").As() == 123); + Assert.IsTrue(ctx.Eval("this.lol").As() == 123); + } + + [TestMethod] + public void ShadowRealmEvaluateExpressionAndReturnNumber() + { + var ctx = new Context().AddShadowRealm(); + ctx.Eval(@" +const realm = new ShadowRealm() +var result = realm.evaluate(`(function () { +globalThis.lol = 123 +return globalThis.lol })()`)"); + + Assert.IsTrue(ctx.GetVariable("result").As() == 123); + } + + [TestMethod] + public void ShadowRealmGlobalThisNotContextModuleThis() + { + var ctx = new Context().AddShadowRealm(); + ctx.Eval(@" +const realm = new ShadowRealm() +var result = globalThis === realm.evaluate(`globalThis`)"); + + Assert.IsTrue(ctx.GetVariable("result").As() == false); + } + + [TestMethod] + public void ShadowRealmImportValueMustCallModuleResolver() + { + var ctx = new Context().AddShadowRealm(new[] + { + new DelegateModuleResolver(((ModuleRequest request, out Module result) => + { + result = null; + if (request.AbsolutePath == "/test.js") + { + result = new Module("export default testing = 123"); + return true; + } + + return false; + })) + }); + ctx.Eval(@"async function test() { +const realm = new ShadowRealm() +var result = await realm.importValue('./test.js', 'default') +return result +}"); + + + Assert.IsTrue( + ctx.GetVariable("test").As().Call(new Arguments()).As().Task.Result.As() == 123); + } + + [TestMethod] + public void ShadowRealmInModuleShouldWork() + { + var ctx = new Module(@" +const realm = new ShadowRealm() +export default async function() { +return await realm.importValue('./test.js', 'default') +} ").AddShadowRealm(new[] + { + new DelegateModuleResolver(((ModuleRequest request, out Module result) => + { + result = null; + if (request.AbsolutePath == "/test.js") + { + result = new Module("export default testing = 123"); + return true; + } + + return false; + })) + }); + ctx.Run(); + + Assert.IsTrue(ctx.Exports.Default.As().Call(new Arguments()).As().Task.Result.As() == + 123); + } +} \ No newline at end of file From ab9403a7abd983dc56bacd39adab3cba47a09581 Mon Sep 17 00:00:00 2001 From: ZverGuy Date: Sat, 13 May 2023 19:39:33 +0300 Subject: [PATCH 2/2] Workarounds for implementing a ShadowRealm --- NiL.JS/BaseLibrary/ShadowRealm.cs | 12 +++++++++++- NiL.JS/Extensions/ContextExtensions.cs | 7 ++++--- Tests/ShadowRealmTest.cs | 11 +++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/NiL.JS/BaseLibrary/ShadowRealm.cs b/NiL.JS/BaseLibrary/ShadowRealm.cs index 4df28d3ef..9c95ee124 100644 --- a/NiL.JS/BaseLibrary/ShadowRealm.cs +++ b/NiL.JS/BaseLibrary/ShadowRealm.cs @@ -1,3 +1,4 @@ +using System.Threading; using System.Threading.Tasks; using NiL.JS.Core; using NiL.JS.Core.Interop; @@ -14,7 +15,16 @@ public sealed class ShadowRealm internal ShadowRealm(IModuleResolver[] allowedModules) { - _mod = new Module("", ""); + GlobalContext ctx = null; + + //We spawn new thread because not possible to create new GlobalContext in current thread when we run context + var t = new Thread(() => + { + ctx = new GlobalContext(); + }); + t.Start(); + t.Join(); + _mod = new Module("", Script.Parse(""), ctx); _mod.Context.DefineVariable("globalThis").Assign(_mod.Context.ThisBind); foreach (var moduleResolver in allowedModules) { diff --git a/NiL.JS/Extensions/ContextExtensions.cs b/NiL.JS/Extensions/ContextExtensions.cs index 782321e39..24aabbed0 100644 --- a/NiL.JS/Extensions/ContextExtensions.cs +++ b/NiL.JS/Extensions/ContextExtensions.cs @@ -15,12 +15,12 @@ public static void Add(this Context context, string key, object value) /// - /// Add implementation of ShadowRealm API + /// Add implementation of ShadowRealm API (EXPERIMENTAL, work by workarounds) /// /// /// /// that used for importValue - /// At current moment not allowed to use Shadow Realm in Shadow realm + /// At current moment not allowed to use Shadow Realm in Shadow realm due to recursion /// Context with ShadowRealm contructor public static Context AddShadowRealm(this Context context, IModuleResolver[] allowedResolvers = null) { @@ -36,11 +36,12 @@ public static Context AddShadowRealm(this Context context, IModuleResolver[] all /// /// Add implementation of ShadowRealm API to module context + /// (EXPERIMENTAL, work by workarounds) /// /// /// /// that used for importValue. If null - used from ModuleResolversChain - /// At current moment not allowed to use Shadow Realm in Shadow realm + /// At current moment not allowed to use Shadow Realm in Shadow realm due to recursion /// Context with ShadowRealm contructor public static Module AddShadowRealm(this Module module, IModuleResolver[] allowedResolvers = null) { diff --git a/Tests/ShadowRealmTest.cs b/Tests/ShadowRealmTest.cs index 671c83101..02de8e0b3 100644 --- a/Tests/ShadowRealmTest.cs +++ b/Tests/ShadowRealmTest.cs @@ -51,6 +51,17 @@ public void ShadowRealmEvaluateExpressionAndReturnNumber() Assert.IsTrue(ctx.GetVariable("result").As() == 123); } + [TestMethod] + public void ShadowRealmChangingPrimitiveInRealmDontAffectOnGlobal() + { + var ctx = new Context().AddShadowRealm(); + ctx.Eval(@" +const realm = new ShadowRealm() +realm.evaluate(`Array.prototype.patch = function() {}`) +var result = Array.prototype.patch"); + + Assert.IsTrue(ctx.GetVariable("result").As() == null); + } [TestMethod] public void ShadowRealmGlobalThisNotContextModuleThis()