diff --git a/Unity/Package/Runtime/V8/SplitProxy/V8SplitProxyHelpers.cs b/Unity/Package/Runtime/V8/SplitProxy/V8SplitProxyHelpers.cs index b191e1b8..7160f16d 100644 --- a/Unity/Package/Runtime/V8/SplitProxy/V8SplitProxyHelpers.cs +++ b/Unity/Package/Runtime/V8/SplitProxy/V8SplitProxyHelpers.cs @@ -778,7 +778,7 @@ internal StdV8ValueArray(Ptr pArray) { ptr = pArray; owns = false; - + data = pArray != Ptr.Null ? V8SplitProxyNative.Instance.StdV8ValueArray_GetData(ptr) : V8Value.Ptr.Null; } @@ -962,6 +962,15 @@ public Decoded Decode() }); } + /// + /// Store a in the wrapped V8Value as a . + /// + /// The value to store. + public void SetBigInt(BigInteger value) + { + SetBigInt(ptr, value); + } + /// /// Store a in the wrapped V8Value as a . /// @@ -971,6 +980,15 @@ public void SetBoolean(bool value) SetBoolean(ptr, value); } + /// + /// Store a in the wrapped V8Value as a . + /// + /// The value to store. + public void SetDateTime(DateTime value) + { + SetDateTime(ptr, value); + } + /// /// Store a pointer to a host object in the wrapped V8Value as a /// or . @@ -1010,6 +1028,15 @@ public void SetNull() SetNull(ptr); } + /// + /// Store a in the wrapped V8Value as a . + /// + /// The value to store. + public void SetNumber(double value) + { + SetNumeric(ptr, value); + } + /// /// Store a in the wrapped V8Value as a or /// . @@ -1023,6 +1050,15 @@ public void SetString(string value) SetNull(ptr); } + /// + /// Store a in the wrapped V8Value as a . + /// + /// The value to store. + public void SetUInt32(uint value) + { + SetNumeric(ptr, value); + } + internal const int Size = 16; internal static IScope CreateScope() @@ -1602,7 +1638,7 @@ public void Dispose() if (Type == Type.V8Object) V8SplitProxyNative.Instance.V8Entity_DestroyHandle((V8Entity.Handle)PtrOrHandle); } - + /// /// Check that the value is a and return it as a /// . @@ -1617,6 +1653,26 @@ public readonly bool GetBoolean() return Int32Value != 0; } + /// + /// Chech that the value is a and return it as a + /// . + /// + /// The value as a . + /// + /// The must have been created with the + /// flag set for this method to + /// work. Else, the Date object will be passed from JavaScript as a + /// . + /// + public readonly DateTime GetDateTime() + { + if (Type != Type.DateTime) + throw new InvalidCastException($"Tried to get a DateTime out of a {GetTypeName()}"); + + return new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc) + + TimeSpan.FromMilliseconds(DoubleValue); + } + /// /// Check that the value is a and return it as a /// . @@ -2173,7 +2229,7 @@ public void Invoke(StdV8ValueArray args, V8Value result, bool asConstructor = fa V8Value.Ptr pResult = result.ptr; V8SplitProxyNative.Invoke(instance => instance.V8Object_Invoke(hObject, asConstructor, pArgs, pResult)); } - + #region Nested type: Handle internal readonly struct Handle diff --git a/Unity/Package/Runtime/V8/V8ScriptEngine.cs b/Unity/Package/Runtime/V8/V8ScriptEngine.cs index 9a83fe00..e1f8e438 100644 --- a/Unity/Package/Runtime/V8/V8ScriptEngine.cs +++ b/Unity/Package/Runtime/V8/V8ScriptEngine.cs @@ -1482,14 +1482,14 @@ internal override void AddHostItem(string itemName, HostItemFlags flags, object ScriptInvoke(() => { - object marshaledItem; - + object marshaledItem; + #if NETCOREAPP || NETSTANDARD - if (item is SplitProxy.IV8HostObject) + if (item is SplitProxy.IV8HostObject or SplitProxy.InvokeHostObject) { marshaledItem = item; } - else + else #endif { marshaledItem = MarshalToScript(item, flags); diff --git a/Unity/Package/Tests.meta b/Unity/Package/Tests.meta new file mode 100644 index 00000000..af7cc811 --- /dev/null +++ b/Unity/Package/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 147b696d1e7c1ae4ab6fd8afe6a03c03 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity/Package/Tests/Runtime.meta b/Unity/Package/Tests/Runtime.meta new file mode 100644 index 00000000..488843b1 --- /dev/null +++ b/Unity/Package/Tests/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8abdc9e70232bad42aac5f1f63438e66 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity/Package/Tests/Runtime/Decentraland.ClearScript.Tests.asmdef b/Unity/Package/Tests/Runtime/Decentraland.ClearScript.Tests.asmdef new file mode 100644 index 00000000..271a1d06 --- /dev/null +++ b/Unity/Package/Tests/Runtime/Decentraland.ClearScript.Tests.asmdef @@ -0,0 +1,25 @@ +{ + "name": "Decentraland.ClearScript.Tests", + "rootNamespace": "Microsoft.ClearScript.Tests", + "references": [ + "GUID:9659fcb6593e30b46909c1245ed056df" + ], + "includePlatforms": [ + "Editor", + "LinuxStandalone64", + "macOSStandalone", + "WindowsStandalone64" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "Microsoft.CodeAnalysis.dll", + "Microsoft.CSharp.dll", + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": true +} \ No newline at end of file diff --git a/Unity/Package/Tests/Runtime/Decentraland.ClearScript.Tests.asmdef.meta b/Unity/Package/Tests/Runtime/Decentraland.ClearScript.Tests.asmdef.meta new file mode 100644 index 00000000..dff847c3 --- /dev/null +++ b/Unity/Package/Tests/Runtime/Decentraland.ClearScript.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b50321a98948f744f8b50465c70c7d5a +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Unity/Package/Tests/Runtime/V8HostObjectTest.cs b/Unity/Package/Tests/Runtime/V8HostObjectTest.cs new file mode 100644 index 00000000..7aa87247 --- /dev/null +++ b/Unity/Package/Tests/Runtime/V8HostObjectTest.cs @@ -0,0 +1,443 @@ +using System; +using System.Globalization; +using Microsoft.ClearScript.V8; +using Microsoft.ClearScript.V8.SplitProxy; +using NUnit.Framework; + +namespace Microsoft.ClearScript.Test +{ + [TestFixture, Parallelizable(ParallelScope.Fixtures)] + internal sealed class V8HostObjectTest + { + private V8ScriptEngine engine; + private HostObject hostObject; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + engine = new V8ScriptEngine(V8ScriptEngineFlags.EnableDateTimeConversion); + hostObject = new HostObject(); + engine.AddHostObject("hostObject", hostObject); + + engine.AddHostObject("assert", new InvokeHostObject((args, _) => + { + if (args.Length != 1) + throw new ArgumentException($"Expected 1 argument, but got {args.Length}"); + + Assert.That(args[0].GetBoolean()); + })); + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + engine.Dispose(); + } + + [TearDown] + public void TearDown() + { + hostObject.GetNamedProperty = null; + hostObject.SetNamedProperty = null; + hostObject.DeleteNamedProperty = null; + hostObject.GetIndexedProperty = null; + hostObject.SetIndexedProperty = null; + hostObject.DeleteIndexedProperty = null; + hostObject.GetEnumerator = null; + hostObject.GetAsyncEnumerator = null; + hostObject.GetNamedPropertyNames = null; + hostObject.GetIndexedPropertyIndices = null; + } + + [Test] + public void DeleteIndexedProperty() + { + bool wasCalled = false; + + hostObject.DeleteIndexedProperty = index => + { + wasCalled = true; + Assert.That(index, Is.EqualTo(42)); + return true; + }; + + engine.Execute(@"{ + delete hostObject[42]; + }"); + + Assert.That(wasCalled); + } + + [Test] + public void DeleteNamedProperty() + { + bool wasCalled = false; + + hostObject.DeleteNamedProperty = name => + { + wasCalled = true; + Assert.That(name.Equals("deleteProperty")); + return true; + }; + + engine.Execute(@"{ + delete hostObject.deleteProperty; + }"); + + Assert.That(wasCalled); + } + + [Test] + public void GetBoolean() + { + bool wasCalled = false; + + hostObject.GetNamedProperty = (StdString name, V8Value value, out bool isConst) => + { + wasCalled = true; + isConst = false; + Assert.That(name.Equals("getBoolean")); + value.SetBoolean(true); + }; + + object result = engine.Evaluate(@"{ + let value = hostObject.getBoolean; + assert(value === true); + value; + }"); + + Assert.That(wasCalled); + Assert.That(result, Is.TypeOf()); + Assert.That(result, Is.True); + } + + [Test] + public void GetDateTime() + { + bool wasCalled = false; + + var ponyEpoch = DateTime.Parse("2010-10-10T20:30:00Z", CultureInfo.InvariantCulture, + DateTimeStyles.AdjustToUniversal); + + hostObject.GetNamedProperty = (StdString name, V8Value value, out bool isConst) => + { + wasCalled = true; + isConst = false; + Assert.That(name.Equals("getDateTime")); + value.SetDateTime(ponyEpoch); + }; + + object result = engine.Evaluate(@"{ + let value = hostObject.getDateTime; + assert(value instanceof Date); + let ponyEpoch = new Date('2010-10-10T20:30:00Z'); + assert(value.getTime() === ponyEpoch.getTime()); + value; + }"); + + Assert.That(wasCalled); + Assert.That(result, Is.TypeOf()); + Assert.That(result, Is.EqualTo(ponyEpoch)); + } + + [Test] + public void GetHostObject() + { + bool wasCalled = false; + + hostObject.GetNamedProperty = (StdString name, V8Value value, out bool isConst) => + { + wasCalled = true; + isConst = false; + Assert.That(name.Equals("getHostObject")); + value.SetHostObject(hostObject); + }; + + object result = engine.Evaluate(@"{ + let value = hostObject.getHostObject; + assert(value === hostObject); + value; + }"); + + Assert.That(wasCalled); + Assert.That(result, Is.TypeOf()); + Assert.That(result, Is.EqualTo(hostObject)); + } + + [Test] + public void GetIndexedProperty() + { + bool wasCalled = false; + + hostObject.GetIndexedProperty = (index, value) => + { + wasCalled = true; + Assert.That(index == 13); + value.SetString("Bing bong!"); + }; + + object result = engine.Evaluate(@"{ + let value = hostObject[13]; + assert(value === 'Bing bong!'); + value; + }"); + + Assert.That(wasCalled); + Assert.That(result, Is.TypeOf()); + Assert.That(result, Is.EqualTo("Bing bong!")); + } + + [Test] + public void GetInt32() + { + bool wasCalled = false; + + hostObject.GetNamedProperty = (StdString name, V8Value value, out bool isConst) => + { + wasCalled = true; + isConst = false; + Assert.That(name.Equals("getInt32")); + value.SetInt32(-273); + }; + + object result = engine.Evaluate(@"{ + let value = hostObject.getInt32; + assert(value == -273); + value; + }"); + + Assert.That(wasCalled); + Assert.That(result, Is.TypeOf()); + Assert.That(result, Is.EqualTo(-273)); + } + + [Test] + public void GetNumber() + { + bool wasCalled = false; + + hostObject.GetNamedProperty = (StdString name, V8Value value, out bool isConst) => + { + wasCalled = true; + isConst = false; + Assert.That(name.Equals("getNumber")); + value.SetNumber(Math.PI); + }; + + object result = engine.Evaluate(@"{ + let value = hostObject.getNumber; + assert(value === 3.1415926535897931); + value; + }"); + + Assert.That(wasCalled); + Assert.That(result, Is.TypeOf()); + Assert.That(result, Is.EqualTo(Math.PI)); + } + + [Test] + public void GetString() + { + bool wasCalled = false; + + hostObject.GetNamedProperty = (StdString name, V8Value value, out bool isConst) => + { + wasCalled = true; + isConst = false; + Assert.That(name.Equals("getString")); + value.SetString("Bing bong!"); + }; + + object result = engine.Evaluate(@"{ + let value = hostObject.getString; + assert(value ==='Bing bong!'); + value; + }"); + + Assert.That(wasCalled); + Assert.That(result, Is.TypeOf()); + Assert.That(result, Is.EqualTo("Bing bong!")); + } + + [Test] + public void SetBoolean() + { + bool wasCalled = false; + + hostObject.SetNamedProperty = (name, value) => + { + wasCalled = true; + Assert.That(name.Equals("setBoolean")); + Assert.That(value.GetBoolean(), Is.True); + }; + + engine.Execute(@"{ + hostObject.setBoolean = true; + }"); + + Assert.That(wasCalled); + } + + [Test] + public void SetDateTime() + { + bool wasCalled = false; + + var ponyEpoch = DateTime.Parse("2010-10-10T20:30:00Z", CultureInfo.InvariantCulture, + DateTimeStyles.AdjustToUniversal); + + hostObject.SetNamedProperty = (name, value) => + { + wasCalled = true; + Assert.That(name.Equals("setDateTime")); + Assert.That(value.GetDateTime(), Is.EqualTo(ponyEpoch)); + }; + + engine.Execute(@"{ + hostObject.setDateTime = new Date('2010-10-10T20:30:00Z'); + }"); + + Assert.That(wasCalled); + } + + [Test] + public void SetHostObject() + { + bool wasCalled = false; + + hostObject.SetNamedProperty = (name, value) => + { + wasCalled = true; + Assert.That(name.Equals("setHostObject")); + Assert.That(value.GetHostObject(), Is.EqualTo(hostObject)); + }; + + engine.Execute(@"{ + hostObject.setHostObject = hostObject; + }"); + + Assert.That(wasCalled); + } + + [Test] + public void SetIndexedProperty() + { + bool wasCalled = false; + + hostObject.SetIndexedProperty = (index, value) => + { + wasCalled = true; + Assert.That(index, Is.EqualTo(13)); + Assert.That(value.GetString(), Is.EqualTo("Bing bong!")); + }; + + engine.Execute(@"{ + hostObject[13] = 'Bing bong!'; + }"); + + Assert.That(wasCalled); + } + + [Test] + public void SetNumber() + { + bool wasCalled = false; + + hostObject.SetNamedProperty = (name, value) => + { + wasCalled = true; + Assert.That(name.Equals("setNumber")); + Assert.That(value.GetNumber(), Is.EqualTo(Math.PI)); + }; + + engine.Execute(@"{ + hostObject.setNumber = 3.1415926535897931; + }"); + + Assert.That(wasCalled); + } + + [Test] + public void SetString() + { + bool wasCalled = false; + + hostObject.SetNamedProperty = (name, value) => + { + wasCalled = true; + Assert.That(name.Equals("setString")); + Assert.That(value.GetString(), Is.EqualTo("Bing bong!")); + }; + + engine.Execute(@"{ + hostObject.setString = 'Bing bong!'; + }"); + + Assert.That(wasCalled); + } + + private sealed class HostObject : IV8HostObject + { + public GetNamedPropertyCallback GetNamedProperty; + public SetNamedPropertyCallback SetNamedProperty; + public DeleteNamedPropertyCallback DeleteNamedProperty; + public GetIndexedPropertyCallback GetIndexedProperty; + public SetIndexedPropertyCallback SetIndexedProperty; + public DeleteIndexedPropertyCallback DeleteIndexedProperty; + public GetEnumeratorCallback GetEnumerator; + public GetAsyncEnumeratorCallback GetAsyncEnumerator; + public GetNamedPropertyNamesCallback GetNamedPropertyNames; + public GetIndexedPropertyIndicesCallback GetIndexedPropertyIndices; + + void IV8HostObject.GetNamedProperty(StdString name, V8Value value, out bool isConst) => + GetNamedProperty(name, value, out isConst); + + void IV8HostObject.SetNamedProperty(StdString name, V8Value.Decoded value) => + SetNamedProperty(name, value); + + bool IV8HostObject.DeleteNamedProperty(StdString name) => + DeleteNamedProperty(name); + + void IV8HostObject.GetIndexedProperty(int index, V8Value value) => + GetIndexedProperty(index, value); + + void IV8HostObject.SetIndexedProperty(int index, V8Value.Decoded value) => + SetIndexedProperty(index, value); + + bool IV8HostObject.DeleteIndexedProperty(int index) => + DeleteIndexedProperty(index); + + void IV8HostObject.GetEnumerator(V8Value result) => + GetEnumerator(result); + + void IV8HostObject.GetAsyncEnumerator(V8Value result) => + GetAsyncEnumerator(result); + + void IV8HostObject.GetNamedPropertyNames(StdStringArray names) => + GetNamedPropertyNames(names); + + void IV8HostObject.GetIndexedPropertyIndices(StdInt32Array indices) => + GetIndexedPropertyIndices(indices); + } + + private delegate void GetNamedPropertyCallback(StdString name, V8Value value, out bool isConst); + + private delegate void SetNamedPropertyCallback(StdString name, V8Value.Decoded value); + + private delegate bool DeleteNamedPropertyCallback(StdString name); + + private delegate void GetIndexedPropertyCallback(int index, V8Value value); + + private delegate void SetIndexedPropertyCallback(int index, V8Value.Decoded value); + + private delegate bool DeleteIndexedPropertyCallback(int index); + + private delegate void GetEnumeratorCallback(V8Value result); + + private delegate void GetAsyncEnumeratorCallback(V8Value result); + + private delegate void GetNamedPropertyNamesCallback(StdStringArray names); + + private delegate void GetIndexedPropertyIndicesCallback(StdInt32Array indices); + } +} \ No newline at end of file diff --git a/Unity/Package/Tests/Runtime/V8HostObjectTest.cs.meta b/Unity/Package/Tests/Runtime/V8HostObjectTest.cs.meta new file mode 100644 index 00000000..5756ebec --- /dev/null +++ b/Unity/Package/Tests/Runtime/V8HostObjectTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a37388c0309546348a5894a6034ca443 +timeCreated: 1738331406 \ No newline at end of file diff --git a/Unity/PackageBuilder/Program.cs b/Unity/PackageBuilder/Program.cs index 8a0f67df..7f4b696d 100644 --- a/Unity/PackageBuilder/Program.cs +++ b/Unity/PackageBuilder/Program.cs @@ -16,9 +16,9 @@ foreach (string file in Directory.GetFiles(srcPath, "*.cs", SearchOption.AllDirectories)) { - if (file.Contains(@"\Windows\") - || file.Contains(@"\AssemblyInfo.") && !file.Contains(".Core.") - || file.Contains(@"\ICUData\") + if (file.Contains(@"\ICUData\") + || file.Contains(@"\Properties\") && !file.EndsWith(@"\AssemblyInfo.Core.cs") + || file.Contains(@"\Windows\") || file.Contains(".Net5.") || file.Contains(".NetCore.") || file.Contains(".NetFramework.") @@ -82,7 +82,73 @@ } } - foreach (string metaFile in Directory.GetFiles(dstPath, "*.meta", SearchOption.AllDirectories)) + DeleteEmptyFolders(dstPath); +} +catch (Exception ex) +{ + Console.WriteLine(ex); + Console.ReadKey(true); + throw; +} + +// TODO: Write tests in MSTest and convert them to NUnit. +/*try +{ + string srcPath = @"..\..\..\..\..\ClearScriptTest"; + string dstPath = @"..\..\..\..\Package\Tests\Runtime"; + + foreach (string file in Directory.GetFiles(dstPath, "*.cs", SearchOption.AllDirectories)) + { + File.Delete(file); + } + + foreach (string file in Directory.GetFiles(srcPath, "*.cs", SearchOption.AllDirectories)) + { + if (file.Contains(".NetCore.") + || file.Contains(".NetFramework.")) + { + continue; + } + + string dstFile = string.Concat(dstPath, file.AsSpan(srcPath.Length)); + Directory.CreateDirectory(Path.GetDirectoryName(dstFile)!); + using var reader = new StreamReader(file); + using var writer = new StreamWriter(dstFile); + writer.NewLine = "\n"; + + while (true) + { + string? line = reader.ReadLine(); + + if (line == null) + break; + else if (line == "using Microsoft.VisualStudio.TestTools.UnitTesting;") + writer.WriteLine("using NUnit.Framework;"); + else if (line == " [TestClass]") + writer.WriteLine(" [TestFixture]"); + else if (line == " [TestInitialize]") + writer.WriteLine(" [SetUp]"); + else if (line == " [TestCleanup]") + writer.WriteLine(" [TearDown]"); + else if (line.StartsWith(" [TestMethod, TestCategory(\"")) + writer.WriteLine(" [Test]"); + else + writer.WriteLine(line); + } + } + + DeleteEmptyFolders(dstPath); +} +catch (Exception ex) +{ + Console.WriteLine(ex); + Console.ReadKey(true); + throw; +}*/ + +static void DeleteEmptyFolders(string path) +{ + foreach (string metaFile in Directory.GetFiles(path, "*.meta", SearchOption.AllDirectories)) { string fileOrFolder = metaFile[..^".meta".Length]; @@ -103,7 +169,7 @@ File.Delete(metaFile); } - foreach (string folder in Directory.GetDirectories(dstPath, "", SearchOption.AllDirectories)) + foreach (string folder in Directory.GetDirectories(path, "", SearchOption.AllDirectories)) { try { @@ -113,9 +179,3 @@ catch (DirectoryNotFoundException) { } } } -catch (Exception ex) -{ - Console.WriteLine(ex); - Console.ReadKey(true); - throw; -}