-
Notifications
You must be signed in to change notification settings - Fork 209
Description
Hi, thank you for the work you are putting into the development of this engine. I come with a question than a problem regarding the engine itself.
For some time now, I've been trying to make a QuickJS port (based on this repository) for React Native, allowing QuickJS to be used as the main engine for processing JS code in mobile apps. However, I have encountered a rather significant problem, related to the use of so-called HostObjects.
The so-called new architecture in React Native enables communication between the JavaScript engine and native modules (e.g. written in C++, Java, Kotlin, Swift or Objective-C) using the JSI (JavaScript Interface). One of the mechanisms used is HostObject, which allows native objects to be used within JS.
I created the JSI layer from scratch, based on the engine documentation, and modelled the HostObject using the JSClassExoticMethods structure and the get_own_property_names, define_own_property, get_own_property methods, respectively.
In React Native, there is a caching mechanism when querying HostObjects. Each HostObject is set as the __proto__ of a regular JSObject. That is, in order to minimise the time it takes to retrieve a value, if a property does not exist in the JSObject it is retrieved from the HostObject and then saved as a regular JSObject property so that the next time it is retrieved, the HostObject does not need to be queried again for the data needed.
With the get_own_property method defined, the mechanism looks like this:
- Get property
testfrom JSObject. - Check if property
testexists in JSObject :- if yes return the value,
- if not we look up in the prototype chain.
- Check if property exists in HostObject, so we call
get_own_property. get_own_propertyreturns a value, and the cache mechanism tries to cache this value in JSObject. To do this it checks if such a field does not exist in the prototype string to store the value.- In each step, however,
get_own_propertyis called to check whether the element exists in the object or not. - There is a looping execution of the function
get_own_property→ ... →JS_SetPropertyInternal2
In the code itself, the function responsible for such retrieval can be found here:

Attempts to solve the problem
I tried to solve this by adding get_property and set_property functions in the JSClassExoticMethods structure. However, in the case of set_property and needing to set a value in JSObject, this is intercepted by __proto__ and set_property of the HostObject.
I have created tests for this purpose, which may describe the situation more clearly:
- Test to check the independence of JSObject and HostObject
TEST_P(JSITest, HostObjectImprovements) {class ConstantHostObject : public HostObject {
Value get(Runtime&, const PropNameID& sym) override {
return 1001;
}
void set(Runtime&, const PropNameID&, const Value& val) override {
EXPECT_EQ(val.getNumber(), 1000);
}
};
Object cho = Object::createFromHostObject(rt, std::make_shared<ConstantHostObject>());
rt.global().setProperty(rt, "ho", cho);
// When I do a set on HostObject it sets the value in HostObject
eval("ho.test = 1000;");
// When I do a set on JSObject with proto HostObject it sets the value in JSObject
eval("({__proto__: ho}).test = 9000;");
// When I do a get on HostObject it takes a value from HostObject
EXPECT_EQ(eval("ho.test").getNumber(), 1001);
// When I do a get on JSObject and the value exists, I take it from JSObject
EXPECT_EQ(eval("({__proto__: ho, test: 9000}).test").getNumber(), 9000);
// When I do a get on JSObject and the value does not exist, I take it from HostObject
EXPECT_EQ(eval("({__proto__: ho, test: 9000}).test2").getNumber(), 1001);
}- Test to check the independence of the get and set methods in HostObject (checks if by chance when calling set get it does not return an error -> this means that set uses get underneath).
TEST_P(JSITest, HostObjectTest) {
class ThrowingHostObject : public HostObject {
Value get(Runtime& rt, const PropNameID& sym) override {
throw std::runtime_error("Cannot get");
}
void set(Runtime& rt, const PropNameID& sym, const Value& val) override {
throw std::runtime_error("Cannot set");
}
};
Object thro = Object::createFromHostObject(rt, std::make_shared<ThrowingHostObject>());
EXPECT_TRUE(thro.isHostObject(rt));
std::string exc = "";
try {
function("function (obj) { obj.thing = 'hello'; }").call(rt, thro);
} catch (const JSError& ex) {
exc = ex.what();
}
EXPECT_NE(exc.find("Cannot set"), std::string::npos);
}I also found some other older implementation based on modified code that uses some sort of interceptor mechanism in the object, but this is not something that is used in this repository. https://github.com/bojie-liu/react-native-quickjs/blob/8393c64b2991e423726810f87e5b1a2f4add0136/cpp/engine/quickjs.c#L844
Would you have any idea how using the exotic object structure to achieve independence in saving object property from getting values?

