Skip to content

Commit

Permalink
Workletizable Classes (#6230)
Browse files Browse the repository at this point in the history
## Summary

This pull request features running classes on the Worklet runtime.
Classes aren't supported by Hermes and require polyfilling.

- Class instances can be created in worklets.
- Class instances cannot be sent between runtimes - afaik it's
impossible to create host objects with a prototype.
- Static class methods aren't supported - a feature for the future
perhaps.

Current implementation is a bit naive.
1. We create worklets from all required polyfills.
1. We create __the whole class__ (not the class instance!) in the
worklet.
1. We create the instance of this ad-hoc made class.

We could could put the class in the global scope, however it doesn't
seem exactly necessary at the moment.

You can obtain a worklet class explicitly by adding a ClassProperty
`__workletClass` to it

```ts
// Explicit WorkletClass
class Clazz {
  __workletClass = true;
}
```

or implicitly by placing it in a file with a worklet directive.

```ts
'worklet';
// Implicit WorkletClass
class Clazz {}
```

## Test plan

- [x] Add unit tests
- [x] Add runtime tests suite
  • Loading branch information
tjzel committed Jul 22, 2024
1 parent 5cd9cea commit 72b761a
Show file tree
Hide file tree
Showing 20 changed files with 2,897 additions and 221 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,6 @@ module.exports = {
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'warn',
'jest/valid-expect': 'error',
'react/react-in-jsx-scope': 'off',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export default function RuntimeTestsExample() {
importTest: () => {
require('./tests/plugin/fileWorkletization.test');
require('./tests/plugin/contextObjects.test');
require('./tests/plugin/workletClasses.test');
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 '../../ReJest/RuntimeTestsApi';
import { getThree, implicitContextObject } from './fileWorkletization';
import { ImplicitWorkletClass, getThree, implicitContextObject } from './fileWorkletization';

const SHARED_VALUE_REF = 'SHARED_VALUE_REF';

Expand Down Expand Up @@ -44,4 +44,23 @@ describe('Test file workletization', () => {
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(5);
});

test('WorkletClasses are workletized', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);

useEffect(() => {
runOnUI(() => {
output.value = new ImplicitWorkletClass().getSeven();
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(7);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ export const implicitContextObject = {
return this.getFour() + 1;
},
};

export class ImplicitWorkletClass {
getSix() {
return 6;
}

getSeven() {
return this.getSix() + 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
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 '../../ReJest/RuntimeTestsApi';

const SHARED_VALUE_REF = 'SHARED_VALUE_REF';

class WorkletClass {
__workletClass = true;
value = 0;
getOne() {
return 1;
}

getTwo() {
return this.getOne() + 1;
}

getIncremented() {
return ++this.value;
}
}

describe('Test worklet classes', () => {
test('class works on React runtime', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const clazz = new WorkletClass();

output.value = clazz.getTwo() + clazz.getIncremented() + clazz.getIncremented();

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

test('constructor works on Worklet runtime', async () => {
const ExampleComponent = () => {
useEffect(() => {
runOnUI(() => {
const clazz = new WorkletClass();
clazz.getOne();
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
// TODO: assert no crash here
});

test('class methods work on Worklet runtime', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);

useEffect(() => {
runOnUI(() => {
const clazz = new WorkletClass();
output.value = clazz.getOne();
})();
});

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

test('class instance methods preserve binding', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);

useEffect(() => {
runOnUI(() => {
const clazz = new WorkletClass();
output.value = clazz.getTwo();
})();
});

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

test('class instances preserve state', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);

useEffect(() => {
runOnUI(() => {
const clazz = new WorkletClass();
output.value = clazz.getIncremented() + clazz.getIncremented();
})();
});

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

test('instanceof operator works on Worklet runtime', async () => {
const ExampleComponent = () => {
const output = useSharedValue<boolean | null>(null);
registerValue(SHARED_VALUE_REF, output);

useEffect(() => {
runOnUI(() => {
const clazz = new WorkletClass();
output.value = clazz instanceof WorkletClass;
})();
});

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

// TODO: Add a test that throws when class is sent from React to Worklet runtime.
});
Loading

0 comments on commit 72b761a

Please sign in to comment.