Skip to content

[Question] Use of exotic methods to independently write and read property from an inherited object #797

@lukaszkurantdev

Description

@lukaszkurantdev

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.

Zrzut ekranu 2025-01-7 o 06 48 58

With the get_own_property method defined, the mechanism looks like this:

  1. Get property test from JSObject.
  2. Check if property test exists in JSObject :
    1. if yes return the value,
    2. if not we look up in the prototype chain.
  3. Check if property exists in HostObject, so we call get_own_property.
  4. get_own_property returns 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.
  5. In each step, however, get_own_property is called to check whether the element exists in the object or not.
  6. There is a looping execution of the function get_own_property → ... → JS_SetPropertyInternal2

Zrzut ekranu 2025-01-7 o 06 49 23

In the code itself, the function responsible for such retrieval can be found here:
Zrzut ekranu 2025-01-7 o 06 47 20

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:

  1. 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);
}
  1. 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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions