Skip to content

Commit

Permalink
Workletizable Context Objects (#6229)
Browse files Browse the repository at this point in the history
This pull request introduces the concept of _Context Objects_ on the
Worklet Runtime.

## What?

Context Object is a shareable object that doesn't lose its `this`
binding when it goes to the Worklet Runtime. To define an object as a
Context Object, the user has to define property `__workletObject` on it.
(Should it be `__contextObject`?)

## Why?

Currently it's not possible to refer to an object's methods from its
other methods.

```ts
const obj = {
  foo() {
    console.log("foo");
  }
  bar() {
    this.foo();
  }
}

runOnUI(() => {
  obj.bar(); // Crash, the binding is lost on serialization.
})();
```

## How?

Context Objects are handled as another type of shareable entity on the
React Runtime.
1. The plugin adds a special property `__workletObjectFactory` to
Context Objects. It's a function that creates an identical object.
2. We serialize the factory instead of the object itself.
3. We recreate the object on the Worklet Runtime via ShareableHandle
mechanism.

## Test plan

- [x] Add unit tests
- [x] Add a runtime test suite
  • Loading branch information
tjzel authored Jul 17, 2024
1 parent d6fcab1 commit 4dcf605
Show file tree
Hide file tree
Showing 8 changed files with 515 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React, { useEffect } from 'react';
import { View } from 'react-native';
import { useSharedValue, runOnUI } from 'react-native-reanimated';
import {
render,
wait,
describe,
getRegisteredValue,
registerValue,
test,
expect,
} from '../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi';

const SHARED_VALUE_REF = 'SHARED_VALUE_REF';

describe('Test context objects', () => {
test('methods are workletized', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo() {
return 1;
},
__workletObject: true,
};

useEffect(() => {
runOnUI(() => {
output.value = contextObject.foo();
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(1);
});

test('properties are workletized', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo: () => 1,
__workletObject: true,
};

useEffect(() => {
runOnUI(() => {
output.value = contextObject.foo();
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(1);
});

test('methods preserve implicit context', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo() {
return 1;
},
bar() {
return this.foo() + 1;
},
__workletObject: true,
};

useEffect(() => {
runOnUI(() => {
output.value = contextObject.bar();
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(2);
});

test('methods preserve explicit context', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo() {
return 1;
},
bar() {
return this.foo.call(contextObject) + 1;
},
__workletObject: true,
};

useEffect(() => {
runOnUI(() => {
output.value = contextObject.bar();
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(2);
});

test('methods change the state of the object', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo: 1,
bar() {
this.foo += 1;
},
__workletObject: true,
};

useEffect(() => {
runOnUI(() => {
contextObject.bar();
output.value = contextObject.foo;
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(2);
});

test("the object doesn't persist in memory", async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo: 1,
bar() {
this.foo += 1;
},
__workletObject: true,
};

useEffect(() => {
runOnUI(() => {
contextObject.bar();
output.value = contextObject.foo;
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(2);
await render(<ExampleComponent />);
await wait(100);
const sharedValue2 = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue2.onUI).toBe(2);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,128 @@ var f = function () {
}();"
`;

exports[`babel plugin for context objects creates factories 1`] = `
"var _worklet_14630842371699_init_data = {
code: "function __workletObjectFactory_null1(){return{bar:function(){return'bar';}};}",
location: "/dev/null",
sourceMap: "\\"mock source map\\"",
version: "x.y.z"
};
var foo = {
bar: function bar() {
return 'bar';
},
__workletObjectFactory: function () {
var _e = [new global.Error(), 1, -27];
var __workletObjectFactory_null1 = function __workletObjectFactory_null1() {
return {
bar: function bar() {
return 'bar';
}
};
};
__workletObjectFactory_null1.__closure = {};
__workletObjectFactory_null1.__workletHash = 14630842371699;
__workletObjectFactory_null1.__initData = _worklet_14630842371699_init_data;
__workletObjectFactory_null1.__stackDetails = _e;
return __workletObjectFactory_null1;
}()
};"
`;

exports[`babel plugin for context objects preserves bindings 1`] = `
"var _worklet_13432710970622_init_data = {
code: "function __workletObjectFactory_null1(){return{bar:function(){return'bar';},foobar:function(){return this.bar();}};}",
location: "/dev/null",
sourceMap: "\\"mock source map\\"",
version: "x.y.z"
};
var foo = {
bar: function bar() {
return 'bar';
},
foobar: function foobar() {
return this.bar();
},
__workletObjectFactory: function () {
var _e = [new global.Error(), 1, -27];
var __workletObjectFactory_null1 = function __workletObjectFactory_null1() {
return {
bar: function bar() {
return 'bar';
},
foobar: function foobar() {
return this.bar();
}
};
};
__workletObjectFactory_null1.__closure = {};
__workletObjectFactory_null1.__workletHash = 13432710970622;
__workletObjectFactory_null1.__initData = _worklet_13432710970622_init_data;
__workletObjectFactory_null1.__stackDetails = _e;
return __workletObjectFactory_null1;
}()
};"
`;

exports[`babel plugin for context objects removes marker 1`] = `
"var _worklet_14630842371699_init_data = {
code: "function __workletObjectFactory_null1(){return{bar:function(){return'bar';}};}",
location: "/dev/null",
sourceMap: "\\"mock source map\\"",
version: "x.y.z"
};
var foo = {
bar: function bar() {
return 'bar';
},
__workletObjectFactory: function () {
var _e = [new global.Error(), 1, -27];
var __workletObjectFactory_null1 = function __workletObjectFactory_null1() {
return {
bar: function bar() {
return 'bar';
}
};
};
__workletObjectFactory_null1.__closure = {};
__workletObjectFactory_null1.__workletHash = 14630842371699;
__workletObjectFactory_null1.__initData = _worklet_14630842371699_init_data;
__workletObjectFactory_null1.__stackDetails = _e;
return __workletObjectFactory_null1;
}()
};"
`;

exports[`babel plugin for context objects workletizes regardless of marker value 1`] = `
"var _worklet_14630842371699_init_data = {
code: "function __workletObjectFactory_null1(){return{bar:function(){return'bar';}};}",
location: "/dev/null",
sourceMap: "\\"mock source map\\"",
version: "x.y.z"
};
var foo = {
bar: function bar() {
return 'bar';
},
__workletObjectFactory: function () {
var _e = [new global.Error(), 1, -27];
var __workletObjectFactory_null1 = function __workletObjectFactory_null1() {
return {
bar: function bar() {
return 'bar';
}
};
};
__workletObjectFactory_null1.__closure = {};
__workletObjectFactory_null1.__workletHash = 14630842371699;
__workletObjectFactory_null1.__initData = _worklet_14630842371699_init_data;
__workletObjectFactory_null1.__stackDetails = _e;
return __workletObjectFactory_null1;
}()
};"
`;

exports[`babel plugin for debugging does inject location for worklets in dev builds 1`] = `
"var _worklet_8623346549410_init_data = {
code: "function null1(){const x=1;}",
Expand Down
66 changes: 66 additions & 0 deletions packages/react-native-reanimated/__tests__/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2282,4 +2282,70 @@ describe('babel plugin', () => {
expect(code).toMatchSnapshot();
});
});

describe('for context objects', () => {
it('removes marker', () => {
const input = html`<script>
const foo = {
bar() {
return 'bar';
},
__workletObject: true,
};
</script>`;

const { code } = runPlugin(input);
expect(code).not.toMatch(/__workletObject:\s*/g);
expect(code).toMatchSnapshot();
});

it('creates factories', () => {
const input = html`<script>
const foo = {
bar() {
return 'bar';
},
__workletObject: true,
};
</script>`;

const { code } = runPlugin(input);
expect(code).toContain('__workletObjectFactory');
expect(code).toHaveWorkletData();
expect(code).toMatchSnapshot();
});

it('workletizes regardless of marker value', () => {
const input = html`<script>
const foo = {
bar() {
return 'bar';
},
__workletObject: new RegExp('foo'),
};
</script>`;

const { code } = runPlugin(input);
expect(code).toHaveWorkletData();
expect(code).toMatchSnapshot();
});

it('preserves bindings', () => {
const input = html`<script>
const foo = {
bar() {
return 'bar';
},
foobar() {
return this.bar();
},
__workletObject: true,
};
</script>`;

const { code } = runPlugin(input);
expect(code).toIncludeInWorkletString('this.bar()');
expect(code).toMatchSnapshot();
});
});
});
Loading

0 comments on commit 4dcf605

Please sign in to comment.