Skip to content

Commit

Permalink
Basic implementation of jsi::HostObject on Chakra (#2428)
Browse files Browse the repository at this point in the history
* Basic implementation of jsi::HostObject on Chakra
Chakra JSRT doesn't provide API support to implement Host objects. We've resorted to
ES6 proxies to simulate the behaviour. The current code has some rough edges, for eg. the inspection apis such as
getHostObject, isHostObjecct etc. aren't supported in edge mode. We will fix them in subsequent changes.

* Fixing build
  • Loading branch information
mganandraj authored May 9, 2019
1 parent b7e6123 commit 7b09d9d
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 27 deletions.
149 changes: 139 additions & 10 deletions vnext/Chakra/ChakraJsiRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,8 @@ jsi::Object ChakraJsiRuntime::createObject() {
}

jsi::Object ChakraJsiRuntime::createObject(std::shared_ptr<jsi::HostObject> hostObject) {
throw std::runtime_error("ChakraJsiRuntime::createObject from HostObject is not implemented.");
}

std::shared_ptr<jsi::HostObject> ChakraJsiRuntime::getHostObject(
const jsi::Object& obj) {
throw std::runtime_error("ChakraJsiRuntime::createObject is not implemented.");
jsi::Object proxyTarget = ObjectWithExternalData<HostObjectProxy>::create(*this, new HostObjectProxy(*this, hostObject));
return createProxy(std::move(proxyTarget), createHostObjectProxyHandler());
}

jsi::Value ChakraJsiRuntime::getProperty(
Expand Down Expand Up @@ -404,10 +400,6 @@ bool ChakraJsiRuntime::isFunction(const jsi::Object& obj) const {
return type == JsValueType::JsFunction;
}

bool ChakraJsiRuntime::isHostObject(const jsi::Object& obj) const {
throw std::runtime_error("Unsupported");
}

jsi::Array ChakraJsiRuntime::getPropertyNames(const jsi::Object& obj) {

JsValueRef propertyNamesArrayRef;
Expand Down Expand Up @@ -683,15 +675,60 @@ jsi::Runtime::PointerValue* ChakraJsiRuntime::makePropertyIdValue(
return new ChakraPropertyIdValue(propIdRef);
}


jsi::Runtime::PointerValue* ChakraJsiRuntime::makeObjectValue(
JsValueRef objectRef) const {
if (!objectRef) {
JsCreateObject(&objectRef);
}

ChakraObjectValue* chakraObjValue = new ChakraObjectValue(objectRef);
return chakraObjValue;
}

template<class T>
jsi::Runtime::PointerValue* ChakraJsiRuntime::makeObjectValue(
JsValueRef objectRef, T* externaldata) const {

if (!externaldata) {
return makeObjectValue(objectRef);
}

// Note :: We explicitly delete the external data proxy when the JS value is finalized.
// The proxy is expected to do the right thing in destructor, for e.g. decrease the ref count of a shared resource.
if (!objectRef) {
JsCreateExternalObject(externaldata, [](void* data) {delete data; }, &objectRef);
}
else {
JsSetExternalData(objectRef, externaldata); // TODO : Is there an API to listen to finalization of arbitrary objects ?
}

ChakraObjectValue* chakraObjValue = new ChakraObjectValue(objectRef);
return chakraObjValue;
}

template <class T>
/*static */ jsi::Object ChakraJsiRuntime::ObjectWithExternalData<T>::create(ChakraJsiRuntime& rt, T* externalData) {
return rt.createObject(static_cast<JsValueRef>(nullptr), externalData);
}

template <class T>
/*static */ ChakraJsiRuntime::ObjectWithExternalData<T> ChakraJsiRuntime::ObjectWithExternalData<T>::fromExisting(ChakraJsiRuntime& rt, jsi::Object&& obj) {
return ObjectWithExternalData<T>(rt.cloneObject(getPointerValue(obj)));
}

template <class T>
T* ChakraJsiRuntime::ObjectWithExternalData<T>::getExternalData() {
T* externalData;
JsGetExternalData(static_cast<const ChakraObjectValue*>(getPointerValue(*this))->m_obj, reinterpret_cast<void**>(&externalData));
return externalData;
}

template<class T>
jsi::Object ChakraJsiRuntime::createObject(JsValueRef objectRef, T* externalData) const {
return make<jsi::Object>(makeObjectValue(objectRef, externalData));
}

jsi::Object ChakraJsiRuntime::createObject(JsValueRef obj) const {
return make<jsi::Object>(makeObjectValue(obj));
}
Expand Down Expand Up @@ -878,4 +915,96 @@ std::string ChakraJsiRuntime::JSStringToSTLString(JsValueRef str) {
}


jsi::Function ChakraJsiRuntime::createProxyConstructor() noexcept {
auto buffer = std::make_unique<StringBuffer>("var ctr=function(target, handler) { return new Proxy(target, handler);};ctr;");
jsi::Value hostObjectProxyConstructor = evaluateJavaScriptSimple(*buffer, "proxy_constructor.js");

if (!hostObjectProxyConstructor.isObject() || !hostObjectProxyConstructor.getObject(*this).isFunction(*this))
std::terminate();

return hostObjectProxyConstructor.getObject(*this).getFunction(*this);
}

jsi::Object ChakraJsiRuntime::createProxy(jsi::Object&& target, jsi::Object&& handler) noexcept {
// Note: We are lazy initializing and cachine the constructor.
static jsi::Function proxyConstructor = createProxyConstructor();

jsi::Value hostObjectProxy = proxyConstructor.call(*this, target, handler);

if (!hostObjectProxy.isObject())
std::terminate();

return hostObjectProxy.getObject(*this);
}

jsi::Object ChakraJsiRuntime::createHostObjectProxyHandler() noexcept {
// TODO :: This object can be cached and reused for multiple host objects.

jsi::Object handlerObj = createObject();
std::string getPropName("get"), setPropName("set"), enumeratePropName("enumerate");

handlerObj.setProperty(
*this,
getPropName.c_str(),
createFunctionFromHostFunction(
createPropNameIDFromAscii(getPropName.c_str(), getPropName.size()),
2,
[this](Runtime& rt, const Value& thisVal, const Value* args, size_t count)->Value {
jsi::Object targetObj = args[0].getObject(*this);
jsi::String propStr = args[1].getString(*this);

ObjectWithExternalData<HostObjectProxy> extObject = ObjectWithExternalData<HostObjectProxy>::fromExisting(*this, std::move(targetObj));
HostObjectProxy* externalData = extObject.getExternalData();
return externalData->Get(jsi::PropNameID::forString(*this, propStr));
}
)
);

handlerObj.setProperty(
*this,
setPropName.c_str(),
createFunctionFromHostFunction(
createPropNameIDFromAscii(setPropName.c_str(), setPropName.size()),
3,
[this](Runtime& rt, const Value& thisVal, const Value* args, size_t count)->Value {
jsi::Object targetObj = args[0].getObject(*this);
jsi::String propStr = args[1].getString(*this);
const jsi::Value& propVal = args[2];

ObjectWithExternalData<HostObjectProxy> extObject = ObjectWithExternalData<HostObjectProxy>::fromExisting(*this, std::move(targetObj));
HostObjectProxy* externalData = extObject.getExternalData();
externalData->Set(jsi::PropNameID::forString(*this, propStr), propVal);
return jsi::Value::undefined();
}
)
);

handlerObj.setProperty(
*this,
enumeratePropName.c_str(),
createFunctionFromHostFunction(
createPropNameIDFromAscii(enumeratePropName.c_str(), enumeratePropName.size()),
1,
[this](Runtime& rt, const Value& thisVal, const Value* args, size_t count)->Value {
jsi::Object targetObj = args[0].getObject(*this);

ObjectWithExternalData<HostObjectProxy> extObject = ObjectWithExternalData<HostObjectProxy>::fromExisting(*this, std::move(targetObj));
HostObjectProxy* externalData = extObject.getExternalData();
auto keys = externalData->Enumerator();

auto result = createArray(keys.size());

for (size_t i = 0; i < count; i++) {
std::string keyStr = keys[i].utf8(*this);
result.setValueAtIndex(*this, i, jsi::String::createFromUtf8(*this, keyStr));
}

return result;
}
)
);

return handlerObj;
}

}}} // facebook::jsi::chakraruntime
54 changes: 53 additions & 1 deletion vnext/Chakra/ChakraJsiRuntime.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,47 @@ class ChakraJsiRuntime : public jsi::Runtime {
static uint64_t getRuntimeVersion() { return s_runtimeVersion; }

private:
class HostObjectProxy {
public:
jsi::Value Get(const PropNameID& propNameId)
{
return hostObject_->get(runtime_, propNameId);
}

void Set(const PropNameID& propNameId, const Value& value)
{
hostObject_->set(runtime_, propNameId, value);
}

std::vector<PropNameID> Enumerator()
{
return hostObject_->getPropertyNames(runtime_);
}

HostObjectProxy(ChakraJsiRuntime& rt, const std::shared_ptr<facebook::jsi::HostObject>& hostObject) : runtime_(rt), hostObject_(hostObject) {}
std::shared_ptr<facebook::jsi::HostObject> getHostObject() { return hostObject_; }

private:
ChakraJsiRuntime& runtime_;
std::shared_ptr<facebook::jsi::HostObject> hostObject_;
};

template <class T>
class ObjectWithExternalData : public jsi::Object {
public:
static jsi::Object create(ChakraJsiRuntime & rt, T * externalData);
static ObjectWithExternalData<T> fromExisting(ChakraJsiRuntime& rt, jsi::Object&& obj);

public:
T* getExternalData();
ObjectWithExternalData(const Runtime::PointerValue* value) : Object(const_cast<Runtime::PointerValue*>(value)) {} // TODO :: const_cast

ObjectWithExternalData(ObjectWithExternalData&&) = default;
ObjectWithExternalData& operator=(ObjectWithExternalData&&) = default;
};

template <class T>
friend class ObjectWithExternalData;

class ChakraPropertyIdValue final : public PointerValue {
ChakraPropertyIdValue(JsPropertyIdRef str);
Expand Down Expand Up @@ -134,8 +175,12 @@ class ChakraJsiRuntime : public jsi::Runtime {

private:

jsi::Object createProxy(jsi::Object&& target, jsi::Object&& handler) noexcept;
jsi::Function createProxyConstructor() noexcept;
jsi::Object createHostObjectProxyHandler() noexcept;

std::unique_ptr<const jsi::Buffer> generatePreparedScript(const std::string& sourceURL, const jsi::Buffer& sourceBuffer) noexcept;
void evaluateJavaScriptSimple(const jsi::Buffer& buffer, const std::string& sourceURL);
jsi::Value evaluateJavaScriptSimple(const jsi::Buffer& buffer, const std::string& sourceURL);
bool evaluateSerializedScript(const jsi::Buffer& scriptBuffer, const jsi::Buffer& serializedScriptBuffer, const std::string& sourceURL);

PointerValue* cloneString(const Runtime::PointerValue* pv) override;
Expand Down Expand Up @@ -214,11 +259,18 @@ class ChakraJsiRuntime : public jsi::Runtime {
// Factory methods for creating String/Object
jsi::String createString(JsValueRef stringRef) const;
jsi::PropNameID createPropNameID(JsValueRef stringRef);

template<class T>
jsi::Object createObject(JsValueRef objectRef, T* externalData) const;
jsi::Object createObject(JsValueRef objectRef) const;

// Used by factory methods and clone methods
jsi::Runtime::PointerValue* makeStringValue(JsValueRef str) const;

template<class T>
jsi::Runtime::PointerValue* makeObjectValue(JsValueRef obj, T* externaldata) const;
jsi::Runtime::PointerValue* makeObjectValue(JsValueRef obj) const;

jsi::Runtime::PointerValue* makePropertyIdValue(JsPropertyIdRef propId) const;

inline void checkException(JsErrorCode res);
Expand Down
28 changes: 14 additions & 14 deletions vnext/Chakra/ChakraJsiRuntimeFactory.h
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#pragma once

#include <jsi/jsi.h>

namespace facebook {
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#pragma once

#include <jsi/jsi.h>

namespace facebook {
namespace jsi {
namespace chakraruntime {
namespace chakraruntime {

struct ChakraJsiRuntimeArgs;

std::unique_ptr<jsi::Runtime> makeChakraJsiRuntime(ChakraJsiRuntimeArgs&& args) noexcept;

} // namespace chakraruntime
} // namespace jsi
} // namespace facebook
std::unique_ptr<jsi::Runtime> makeChakraJsiRuntime(ChakraJsiRuntimeArgs&& args) noexcept;

} // namespace chakraruntime
} // namespace jsi
} // namespace facebook
38 changes: 37 additions & 1 deletion vnext/Chakra/ChakraJsiRuntime_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,40 @@ namespace facebook {
namespace jsi {
namespace chakraruntime {

void ChakraJsiRuntime::evaluateJavaScriptSimple(const jsi::Buffer& buffer, const std::string& sourceURL) {
bool ChakraJsiRuntime::isHostObject(const jsi::Object& obj) const {
bool isProxy;
JsValueRef target, handler;
JsGetProxyProperties(objectRef(obj), &isProxy, &target, &handler);

if (isProxy) {
ObjectWithExternalData<HostObjectProxy> extObject = ObjectWithExternalData<HostObjectProxy>::fromExisting(const_cast<ChakraJsiRuntime&>(*this), createObject(target));
if (extObject.getExternalData()) {
return true;
}
}

return false;
}

std::shared_ptr<jsi::HostObject> ChakraJsiRuntime::getHostObject(const jsi::Object& obj) {

bool isProxy;
JsValueRef target, handler;

JsGetProxyProperties(objectRef(obj), &isProxy, &target, &handler);

ObjectWithExternalData<HostObjectProxy> extObject = ObjectWithExternalData<HostObjectProxy>::fromExisting(*this, createObject(target));
HostObjectProxy* externalData = extObject.getExternalData();
if (externalData) {
return externalData->getHostObject();
}
else {
return nullptr;
}
}


Value ChakraJsiRuntime::evaluateJavaScriptSimple(const jsi::Buffer& buffer, const std::string& sourceURL) {
JsValueRef sourceRef;
JsCreateString(reinterpret_cast<const char*>(buffer.data()), buffer.size(), &sourceRef);

Expand All @@ -35,8 +68,11 @@ void ChakraJsiRuntime::evaluateJavaScriptSimple(const jsi::Buffer& buffer, const

JsValueRef result;
checkException(JsRun(sourceRef, 0, sourceURLRef, JsParseScriptAttributes::JsParseScriptAttributeNone, &result), sourceURL.c_str());

return createValue(result);
}

// TODO :: Return result
bool ChakraJsiRuntime::evaluateSerializedScript(const jsi::Buffer& scriptBuffer, const jsi::Buffer& serializedScriptBuffer, const std::string& sourceURL) {
JsValueRef bytecodeArrayBuffer = nullptr;
if (JsCreateExternalArrayBuffer(const_cast<uint8_t *>(serializedScriptBuffer.data()), static_cast<unsigned int>(serializedScriptBuffer.size()), nullptr, nullptr, &bytecodeArrayBuffer) == JsNoError) {
Expand Down
16 changes: 15 additions & 1 deletion vnext/Chakra/ChakraJsiRuntime_edgemode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,18 @@ namespace facebook {
namespace jsi {
namespace chakraruntime {

void ChakraJsiRuntime::evaluateJavaScriptSimple(const jsi::Buffer& buffer, const std::string& sourceURL) {
// edge mode currently doesn't allow us to inspect proxies .. We need to implement these hostObject
// related methods through some bookkeeping in our code.
bool ChakraJsiRuntime::isHostObject(const jsi::Object& obj) const {
throw std::runtime_error("ChakraJsiRuntime::isHostObject is not yet implemented.");
}

std::shared_ptr<jsi::HostObject> ChakraJsiRuntime::getHostObject(
const jsi::Object& obj) {
throw std::runtime_error("ChakraJsiRuntime::createObject is not implemented.");
}

Value ChakraJsiRuntime::evaluateJavaScriptSimple(const jsi::Buffer& buffer, const std::string& sourceURL) {
const std::wstring script16 = facebook::react::UnicodeConversion::Utf8ToUtf16(reinterpret_cast<const char*>(buffer.data()), buffer.size());
if (script16.empty()) throw jsi::JSINativeException("Script can't be empty.");

Expand All @@ -22,8 +33,11 @@ void ChakraJsiRuntime::evaluateJavaScriptSimple(const jsi::Buffer& buffer, const

JsValueRef result;
checkException(JsRunScript(script16.c_str(), JS_SOURCE_CONTEXT_NONE /*sourceContext*/, url16.c_str(), &result));

return createValue(result);
}

// TODO :: Return result
bool ChakraJsiRuntime::evaluateSerializedScript(const jsi::Buffer& scriptBuffer, const jsi::Buffer& serializedScriptBuffer, const std::string& sourceURL) {
std::wstring script16 = facebook::react::UnicodeConversion::Utf8ToUtf16(reinterpret_cast<const char*>(scriptBuffer.data()), scriptBuffer.size());
std::wstring url16 = facebook::react::UnicodeConversion::Utf8ToUtf16(sourceURL);
Expand Down

0 comments on commit 7b09d9d

Please sign in to comment.