Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift Turbo Modules #813

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions proposals/0000-swift-turbo-modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
---
title: Swift Turbo Modules
author:
- Oskar Kwasniewski
- Riccardo Cipolleschi
date: 12-08-2024
---

# RFC0000: Swift Turbo Modules

## Summary

This RFC aims to allow developers to write Turbo Modules using Swift. This will allow the usage of more modern language making maintenance of native modules easier and more accessible.

## Motivation

The primary motivations for introducing Swift Turbo Modules are:
- Enhance developer experience for iOS developers working with React Native
- Allow the use of more modern language
- Lower the entry barrier to write a native turbo module


## Detailed design

One of the reasons why we can't adopt Swift in TurboModules is the contamination of C++ types ending up in user-space.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While i can understand not wanting to expose internal C++ types to user land, reality is that RN is based on C++, and if something should be exposed and used by library maintainers, then having a clear API that defines public vs internal is all that is required, the language is inconsequential. I think as a library developer, regardless of what we do, we will eventually have to debug or deal with those internals, and this should be fine. Though maybe its an unpopular opinion

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely agree with you. But we would like to provide a cleaner and simple experience to library maintainers. Not every library needs to know how react native works under the hoods, no?

We also have an effort internally to try and define the public boundary better, with a clear distinction between what's public and what's private. The project is almost 10 years old in OSS, so work is not trivial and requires a bit of time, but we hope to get there soon!


### Current Situation
The interfaces we generate for an Objective-C turbomodules have this shape:
```objc
@protocol NativeMyTurboModuleSpec <RCTBridgeModule, RCTTurboModule>
@end
```

The `RCTTurboModule` protocol requires the conforming object to implement a method with this signature:
```objc
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params;
```

The signature of this method contains two types from C++:

* `std::shared_ptr<facebook::react::TurboModule>`
* `facebook::react::ObjCTurboModule::InitParams`

### Solution

The idea is to wrap the `getTurboModule` invocation in a `ModuleFactory` object.

The `TurboModuleWrapper` object is a base class that is supposed to be extended by a companion object for TurboModules. The base class has this interface:
```objc
@interface TurboModuleWrapper: NSObject
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params;
@end
```
And the base class implementation just fails, as we don't want for it to be used directly.

Then, the `RCTTurboModule` interface can ask each TM to actually return an implementation of the `TurboModuleWrapper` rather then the actual `TurboModule`.

The `TurboModuleWrapper` is a pure Objective-C class, so it will work seamlessly with ObjectiveC and Swift.

So, now, the public interface of a TurboModule will only have pure objc entries and no C++ code.

When it comes to the implementation, the user-defined TurboModule won't have to deal with any C++ code:

```objc
@implementation MyTurboModule
- (TurboModuleWrapper *)moduleFactory
{
return [[MyTurboModuleWrapper alloc] init];
}

// ... rest of the TM methods ...

@end
```

The `MyTurboModuleWrapper` implementation can be Codegenerated!

We don't even need to ask our users to write that code themselves, as we have all the informations we need in the Codegen already.


The implementation will look like (note that `<UserDefinedName>` is something we get from Codegen):
```objc
// In the .h file

@interface Native<UserDefinedName>Wrapper: TurboModuleWrapper
@end

// In the .mm file

@implementation Native<UserDefinedName>Wrapper

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params;
{
return std::make_shared<facebook::react::Native<UserDefinedName>SpecJSI>(params);
}

@end
```


The user-defined TurboModule has already access to this file, and so the switch from `getTurboModule` to `getWrapper` doesn't require any additional includes.


Finally, we will have to update the `RCTTurboModuleManager` to take this new object into consideration. So we have to modify the provide Turbomodule with the following code
```objc
// Step 2e: Return an exact sub-class of ObjC TurboModule
std::shared_ptr<facebook::react::TurboModule> turboModule = nullptr;

if ([module respondsToSelector:@selector(getTurboModule:)]) {
turboModule = [module getTurboModule:params];
} else if ([module respondsToSelector:@selector(getWrapper)]) {
auto wrapper = [module getWrapper];
turboModule = [wrapper getTurboModule:params];
}
```


Additionally we need to make sure that `ReactCodegen` module is compatible with importing to Swift. I did a small test and adding few ifdefs to React_Codegen headers allows us to use Swift.

```swift
import protocol ReactCodegen.NativeSwiftTestLibrarySpec
import protocol ReactCodegen.TurboModuleWrapper
import class ReactCodegen.NativeSwiftTestLibraryWrapper

@objc public class SwiftTestLibrary: NSObject, NativeSwiftTestLibrarySpec {
@objc public func multiply(_ a: Double, b: Double) -> NSNumber! {
return a * b as NSNumber
}

@objc public static func moduleName() -> String! {
return "SwiftTestLibrary"
}

@objc public func getWrapper() -> (any TurboModuleWrapper)! {
return NativeSwiftTestLibraryWrapper()
}
}


public func SwiftTestLibraryCls() -> AnyClass {
return SwiftTestLibrary.self
}
```

Here is a POC implementation of the proposal: https://github.com/okwasniewski/react-native/commit/93b21d1a2e5769924ae1913e912e94296a92f3d8 (using @protocol).

## Drawbacks

- Additional complexity in codegen
- Setup Swift CI/CD to test if there are no regressions breaking swift builds

## Alternatives

- Use Objective-C for all native modules

- **Using a protocol for the wrapper.**
The pro of this is that we don't have an empty implementation for the TurboModuleWrapper object.
The cons are various:
* The `TurboModule` itself can't adopt the `TurboModuleWrapper` protocol as, otherwise, the C++ signature will come back to the public API of the I don't think this will work.
* We can't codegen the default implementation for the `getTurboModule` as we won't have the base class for the protocol.
* We could use a protocol and create a companion object in the codegen which extends the protocol and it is returned by the TurboModule. This solution works, but adds a bit of ceremonies to the base implementation above.

- **Using a custom base class for each TurboModule**
The only pro of this approach is that we can remove a few lines from the definition of every TurboModule.
The con of this approach are:
* The C++ code remains in the public API of the TurboModule
* We are creating a deeper inheritance chain which usually should be avoided.
* We are asking to all the users to inherit from a different base class. This is hard to make backward compatible.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using the interop between Swift and C++? What is missing here right now when it comes to making modules? And how much work / breaking change would it need for it? I love this work, but going through ObjC is just going to mean every class still needs to deal with some level of ObjC nonsense, like the NSNumber stuff, and it is an additional layer between the code the library author writes and what is happening under the hood.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, what's stopping Swift and C++ interop is the lack of virtual functions support. So this could be viable once Apple adds this feature. Another issue is that the C++ interop doesn't support having C++ in the header files, which is what React Native is doing. Codegen generates Objective-C interface anyways but we need a way of hiding C++ types from Swift and this is where the ModuleProvider comes in.

Copy link

@TheRogue76 TheRogue76 Aug 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2024-08-13 at 13 41 27 Based on https://developer.apple.com/videos/play/wwdc2024/10136/?time=1307 from WWDC24, the first one should not be an issue moving forward when Swift 6 drops. As for the second one I am not sure what the usage is, can you explain that one a bit more?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi there!
There are several concerns here, to be honest.

What @okwasniewski explained is correct. I was in touch with Apple engineer and they mentioned that the inheritance model between Swift and C++ is too different to implement it properly, as of today. There are many cocnerns related to what a virtual class implies in C++ and a protocol in Swift. TL;DR: it will not happen that Swift class can inherit from C++.

On another side, Swift/Cxx interop is too young and unstable to be used concretely, imho.
I'm very scared that if we start introducing it, people that have custom project might incur in weird compilation issues and that maintenance cost will fall on our shoulder.
Instead, objc/swift interop is well documented and supported and, overall, more stable.

I agree that is perhaps non ideal in terms of ergonomics, but we can try to mitigate the problem Codegenerating as much as we can.


## Adoption strategy

- Introduce as an experimental feature in a future React Native release
- Provide comprehensive documentation and migration guides
- This proposal keeps the code backward compatible

## How we teach this

- Create detailed documentation with step-by-step guides
- Update React Native's official documentation to include Swift examples
- Provide sample projects demonstrating real-world use cases