diff --git a/Source/Noesis.Javascript/JavaScript.Net.vcxproj b/Source/Noesis.Javascript/JavaScript.Net.vcxproj index aea6b78..4d60057 100644 --- a/Source/Noesis.Javascript/JavaScript.Net.vcxproj +++ b/Source/Noesis.Javascript/JavaScript.Net.vcxproj @@ -160,6 +160,7 @@ + @@ -168,6 +169,7 @@ + diff --git a/Source/Noesis.Javascript/JavaScript.Net.vcxproj.filters b/Source/Noesis.Javascript/JavaScript.Net.vcxproj.filters index 153155f..d3666f9 100644 --- a/Source/Noesis.Javascript/JavaScript.Net.vcxproj.filters +++ b/Source/Noesis.Javascript/JavaScript.Net.vcxproj.filters @@ -30,6 +30,9 @@ Header Files + + Header Files + @@ -50,6 +53,9 @@ Source Files + + Source Files + diff --git a/Source/Noesis.Javascript/JavascriptContext.cpp b/Source/Noesis.Javascript/JavascriptContext.cpp index c2b39a6..3af39a4 100644 --- a/Source/Noesis.Javascript/JavascriptContext.cpp +++ b/Source/Noesis.Javascript/JavascriptContext.cpp @@ -157,6 +157,7 @@ JavascriptContext::JavascriptContext() mExternals = gcnew System::Collections::Generic::Dictionary(); HandleScope scope(isolate); mContext = new Persistent(isolate, Context::New(isolate)); + mIsDisposed = false; } //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -173,6 +174,7 @@ JavascriptContext::~JavascriptContext() } if (isolate != NULL) isolate->Dispose(); + mIsDisposed = true; } //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -357,6 +359,20 @@ JavascriptContext::GetCurrentIsolate() //////////////////////////////////////////////////////////////////////////////////////////////////// +bool JavascriptContext::IsDisposed() +{ + return mIsDisposed; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +Handle JavascriptContext::GetGlobal() +{ + return mContext->Get(this->GetCurrentIsolate())->Global(); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + v8::Locker * JavascriptContext::Enter([System::Runtime::InteropServices::Out] JavascriptContext^% old_context) { diff --git a/Source/Noesis.Javascript/JavascriptContext.h b/Source/Noesis.Javascript/JavascriptContext.h index 850007d..3174553 100644 --- a/Source/Noesis.Javascript/JavascriptContext.h +++ b/Source/Noesis.Javascript/JavascriptContext.h @@ -172,6 +172,8 @@ public ref class JavascriptContext: public System::IDisposable static v8::Isolate *GetCurrentIsolate(); + Handle GetGlobal(); + v8::Locker *Enter([System::Runtime::InteropServices::Out] JavascriptContext^% old_context); void Exit(v8::Locker *locker, JavascriptContext^ old_context); @@ -179,12 +181,15 @@ public ref class JavascriptContext: public System::IDisposable JavascriptExternal* WrapObject(System::Object^ iObject); Handle GetObjectWrapperTemplate(); - + + bool IsDisposed(); + static void FatalErrorCallbackMember(const char* location, const char* message); //////////////////////////////////////////////////////////// // Data members //////////////////////////////////////////////////////////// + protected: // By entering an isolate before using a context, we can have multiple // contexts used simultaneously in different threads. @@ -202,6 +207,8 @@ public ref class JavascriptContext: public System::IDisposable // the context is destroyed. System::Collections::Generic::Dictionary ^mExternals; + bool mIsDisposed; + // Keeping track of recursion. [System::ThreadStaticAttribute] static JavascriptContext ^sCurrentContext; diff --git a/Source/Noesis.Javascript/JavascriptFunction.cpp b/Source/Noesis.Javascript/JavascriptFunction.cpp new file mode 100644 index 0000000..c890a9c --- /dev/null +++ b/Source/Noesis.Javascript/JavascriptFunction.cpp @@ -0,0 +1,98 @@ +#include "JavascriptFunction.h" +#include "JavascriptInterop.h" +#include "JavascriptContext.h" + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +namespace Noesis { namespace Javascript { + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +JavascriptFunction::JavascriptFunction(v8::Handle iFunction, JavascriptContext^ context) +{ + if (!iFunction->IsFunction()) + throw gcnew System::ArgumentException("Trying to use non-function as function"); + + if(!context) + throw gcnew System::ArgumentException("Must provide a JavascriptContext"); + + mFuncHandle = new Persistent(context->GetCurrentIsolate(), Handle::Cast(iFunction)); + mContext = context; +} + +JavascriptFunction::~JavascriptFunction() +{ + if(mFuncHandle) + { + if (mContext && !mContext->IsDisposed()) + { + JavascriptScope scope(mContext); + mFuncHandle->Reset(); + } + delete mFuncHandle; + mFuncHandle = nullptr; + } + System::GC::SuppressFinalize(this); +} + +JavascriptFunction::!JavascriptFunction() +{ + if(mFuncHandle) + { + if (mContext && !mContext->IsDisposed()) + { + JavascriptScope scope(mContext); + mFuncHandle->Reset(); + } + delete mFuncHandle; + mFuncHandle = nullptr; + } +} + +System::Object^ JavascriptFunction::Call(... cli::array^ args) +{ + JavascriptScope scope(mContext); + HandleScope handleScope(mContext->GetCurrentIsolate()); + + Handle global = mContext->GetGlobal(); + + int argc = args->Length; + Handle *argv = new Handle[argc]; + for (int i = 0; i < argc; i++) + { + argv[i] = JavascriptInterop::ConvertToV8(args[i]); + } + + Local retVal = mFuncHandle->Get(mContext->GetCurrentIsolate())->Call(global, argc, argv); + + delete [] argv; + return JavascriptInterop::ConvertFromV8(retVal); +} + +bool JavascriptFunction::operator==(JavascriptFunction^ func1, JavascriptFunction^ func2) +{ + if(ReferenceEquals(func2, nullptr)) { + return false; + } + Handle jsFuncPtr1 = func1->mFuncHandle->Get(func1->mContext->GetCurrentIsolate()); + Handle jsFuncPtr2 = func2->mFuncHandle->Get(func2->mContext->GetCurrentIsolate()); + + return jsFuncPtr1->Equals(jsFuncPtr2); +} + +bool JavascriptFunction::Equals(JavascriptFunction^ other) +{ + return this == other; +} + +bool JavascriptFunction::Equals(Object^ other) +{ + JavascriptFunction^ otherFunc = dynamic_cast(other); + return (otherFunc && this->Equals(otherFunc)); +} + +} } // namespace Noesis::Javascript + +//////////////////////////////////////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/Source/Noesis.Javascript/JavascriptFunction.h b/Source/Noesis.Javascript/JavascriptFunction.h new file mode 100644 index 0000000..199fe29 --- /dev/null +++ b/Source/Noesis.Javascript/JavascriptFunction.h @@ -0,0 +1,45 @@ +#pragma once + +////////////////////////////////////////////////////////////////////////// + +#include + +#include "JavascriptContext.h" + +using namespace v8; + +////////////////////////////////////////////////////////////////////////// + +namespace Noesis { namespace Javascript { + +////////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////////////////////////////////// +// JavascriptFunction +// +// Wraps around JS function object and allow calling it in later time +////////////////////////////////////////////////////////////////////////// +public ref class JavascriptFunction +{ +public: + JavascriptFunction(v8::Handle iFunction, JavascriptContext^ context); + ~JavascriptFunction(); + !JavascriptFunction(); + + System::Object^ Call(... cli::array^ args); + + static bool operator== (JavascriptFunction^ func1, JavascriptFunction^ func2); + bool Equals(JavascriptFunction^ other); + + virtual bool Equals(Object^ other) override; + +private: + v8::Persistent* mFuncHandle; + JavascriptContext^ mContext; +}; + +////////////////////////////////////////////////////////////////////////// + +} } // namespace Noesis::Javascript + +////////////////////////////////////////////////////////////////////////// \ No newline at end of file diff --git a/Source/Noesis.Javascript/JavascriptInterop.cpp b/Source/Noesis.Javascript/JavascriptInterop.cpp index d6f4de7..03f94cb 100644 --- a/Source/Noesis.Javascript/JavascriptInterop.cpp +++ b/Source/Noesis.Javascript/JavascriptInterop.cpp @@ -33,6 +33,7 @@ #include "SystemInterop.h" #include "JavascriptException.h" #include "JavascriptExternal.h" +#include "JavascriptFunction.h" #include @@ -129,6 +130,8 @@ JavascriptInterop::ConvertFromV8(Handle iValue, ConvertedObjects &already return ConvertArrayFromV8(iValue, already_converted); if (iValue->IsDate()) return ConvertDateFromV8(iValue); + if (iValue->IsFunction()) + return gcnew JavascriptFunction(iValue->ToObject(), JavascriptContext::GetCurrent()); if (iValue->IsObject()) { Handle object = iValue->ToObject(); diff --git a/Tests/Noesis.Javascript.Tests/JavascriptFunctionTests.cs b/Tests/Noesis.Javascript.Tests/JavascriptFunctionTests.cs new file mode 100644 index 0000000..8a40d01 --- /dev/null +++ b/Tests/Noesis.Javascript.Tests/JavascriptFunctionTests.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Noesis.Javascript.Tests +{ + [TestClass] + public class JavascriptFunctionTests + { + private JavascriptContext _context; + + [TestInitialize] + public void SetUp() + { + _context = new JavascriptContext(); + } + + [TestCleanup] + public void TearDown() + { + _context.Dispose(); + } + + [TestMethod] + public void GetFunctionExpressionFromJsContext() + { + _context.Run("a = function(a, b) { return a + b; }"); + + JavascriptFunction funcObj = _context.GetParameter("a") as JavascriptFunction; + funcObj.Should().NotBeNull(); + funcObj.Call(1, 2).Should().BeOfType().Which.Should().Be(3); + } + + [TestMethod] + public void GetNamedFunctionFromJsContext() + { + _context.Run("function test(a, b) { return a + b; }"); + + JavascriptFunction funcObj = _context.GetParameter("test") as JavascriptFunction; + funcObj.Should().NotBeNull(); + funcObj.Call(1, 2).Should().BeOfType().Which.Should().Be(3); + } + + [TestMethod] + public void GetArrowFunctionExpressionFromJsContext() + { + _context.Run("a = (a, b) => a + b"); + + JavascriptFunction funcObj = _context.GetParameter("a") as JavascriptFunction; + funcObj.Should().NotBeNull(); + funcObj.Call(1, 2).Should().BeOfType().Which.Should().Be(3); + } + + [TestMethod] + public void PassFunctionToMethodInManagedObjectAndUseItToFilterAList() + { + _context.SetParameter("collection", new CollectionWrapper()); + + var result = _context.Run("collection.Filter(x => x % 2 === 0)") as IEnumerable; + result.Should().NotBeNull(); + result.Should().BeEquivalentTo(2, 4); + } + } + + class CollectionWrapper + { + private IEnumerable numbers = new List { 1, 2, 3, 4, 5 }; + + public IEnumerable Filter(JavascriptFunction predicate) + { + return numbers.Where(x => (bool) predicate.Call(x)); + } + } +} diff --git a/Tests/Noesis.Javascript.Tests/Noesis.Javascript.Tests.csproj b/Tests/Noesis.Javascript.Tests/Noesis.Javascript.Tests.csproj index a5f0bb9..f647371 100644 --- a/Tests/Noesis.Javascript.Tests/Noesis.Javascript.Tests.csproj +++ b/Tests/Noesis.Javascript.Tests/Noesis.Javascript.Tests.csproj @@ -85,6 +85,7 @@ +