Skip to content

Conversation

@vmoroz
Copy link
Member

@vmoroz vmoroz commented Apr 21, 2020

The main difference between NativeModules and TurboModules is that the TurboModules have a module API spec written in Flow language that can be used to generate a spec in a native language such as C++ so that the API shape is the same between JavaScript and the native code.
Next time when spec is changed, the native code spec is regenerated and any API mismatch could be found at compile time.

In this PR we are introducing the shape of the C++ TurboModule spec and its match against the hand-written TurboModule code.

There are two important design decisions:

  • The spec is not an abstract class with virtual methods to override. While working on the NativeModules 2.0 we have realized that a native module method callable from JavaScript could have completely different signature:
    • It could be an instance or static method.
    • Simple asynchronous method could return value instead of using callbacks.
    • Co-routine methods have different signatures comparing with non-co-routine methods.
    • Method name in C++ could be different from JavaScript because of coding conventions.
  • The code written by developers for the TurboModule is exactly the same as the NativeModules 2.0:
    • Each exported method has a "custom attribute" REACT_METHOD or REACT_SYNC_METHOD to associate the method with a JS name.
    • Exported methods have strongly typed arguments that are converted with help of ReadValue/WriteValue methods.
    • There is no requirement for the base class or interface. The TurboModule/NativeModule is just a struct with custom attributes that can be inherited from any other type or not inherited at all.
    • Since TurboModules and NativeModules 2.0 have exactly the same shape, developers can always start with a simple NativeModule and then 'evolve' it to a TurboModule if they want.

What is the TurboModule spec and how it is verified?
Each TurboModule spec is a class that has two important parts:

  • a tuple with method signatures and names
  • constexpr ValidateModule method that validates the method signatures against the provided module class.

For example, a Turbo module with two asynchronous methods Add and NegatePromise and one synchronous method SayHelloSync could have the following generated code:

struct MyTurboModuleSpec : winrt::Microsoft::ReactNative::TurboModuleSpec {
  static constexpr auto methods = std::tuple{
      Method<void(int, int, Callback<int>) noexcept>{0, L"Add"},
      Method<void(int, Promise<int>) noexcept>{1, L"NegatePromise"},
      SyncMethod<std::string() noexcept>{2, L"SayHelloSync"},
  };

  template <class TModule>
  static constexpr void ValidateModule() noexcept {
    constexpr auto methodCheckResults = CheckMethods<TModule, MyTurboModuleSpec>();

    REACT_SHOW_METHOD_SPEC_ERRORS(
        0,
        "Add",
        "    REACT_METHOD(Add) int Add(int, int) noexcept {/*implementation*/}\n"
        "    REACT_METHOD(Add) void Add(int, int, ReactCallback<int>) noexcept {/*implementation*/}\n"
        "    REACT_METHOD(Add) winrt::fire_and_forget Add(int, int, ReactCallback<int>) noexcept {/*implementation*/}\n"
        "    REACT_METHOD(Add) static int Add(int, int) noexcept {/*implementation*/}\n"
        "    REACT_METHOD(Add) static void Add(int, int, ReactCallback<int>) noexcept {/*implementation*/}\n"
        "    REACT_METHOD(Add) static React::Coroutine Add(int, int, ReactCallback<int>) noexcept {/*implementation*/}\n");
    REACT_SHOW_METHOD_SPEC_ERRORS(
        1,
        "NegatePromise",
        "    REACT_METHOD(NegatePromise) void NegatePromise(int, ReactPromise<int>) noexcept {/*implementation*/}\n"
        "    REACT_METHOD(NegatePromise) winrt::fire_and_forget NegatePromise(int, ReactPromise<int>) noexcept {/*implementation*/}\n"
        "    REACT_METHOD(NegatePromise) static void NegatePromise(int, ReactPromise<int>) noexcept {/*implementation*/}\n"
        "    REACT_METHOD(NegatePromise) static winrt::fire_and_forget NegatePromise(int, ReactPromise<int>) noexcept {/*implementation*/}\n");
    REACT_SHOW_SYNC_METHOD_SPEC_ERRORS(
        2,
        "SayHelloSync",
        "    REACT_METHOD(SayHelloSync) std::string SayHelloSync() noexcept {/*implementation*/}\n"
        "    REACT_METHOD(SayHelloSync) static std::string SayHelloSync() noexcept {/*implementation*/}\n");
  }
};

Note that the biggest part of the generated code are the error messages. The goal is to help developers to write the code that matches the TurboModule specification.
Currently we do the following checks at compile time:

  • There is a method with a REACT_METHOD or REACT_SYNC_METHOD custom attribute that exports it with a name used in the spec. Name of the C++ method could be different from the JS name. We only check the JS name.
  • There are no two methods exported with the same name.
  • The signature of the method is matching the spec. As you can see from the error messages, the same spec could be match multiple implementation signatures.

The module matching the spec could be implemented as the following:

REACT_MODULE(MyTurboModule)
struct MyTurboModule {
  REACT_METHOD(Add)
  int Add(int x, int y) noexcept {
    return x + y;
  }

  REACT_METHOD(Negate, L"NegatePromise")
  void Negate(int x, React::ReactPromise<int> const &result) noexcept {
    if (x >= 0) {
      result.Resolve(-x);
    } else {
      result.Reject("Already negative");
    }
  }

  REACT_SYNC_METHOD(SayHello, L"SayHelloSync")
  static std::string SayHello() noexcept {
    return "Hello";
  }
};

This PR has only the initial change. We plan to iterate over it. The issues to be addressed in future:

  • Check that all exported methods are present in the spec.
  • Do we need spec validation for constants?
  • Take advantage of using short React namespace instead of the long winrt::Microsoft::ReactNative.
  • Better names for some classes and methods.
  • Consider removing numeric indexes in the generated spec.
Microsoft Reviewers: Open in CodeFlow

@vmoroz vmoroz requested a review from a team as a code owner April 21, 2020 14:07
@acoates-ms
Copy link
Contributor

Note, actually the specs/codegen are only part of turbomodules. The other part being that they work on JSI under the covers and dont go through as much JS initialization code which causes bad start up performance. Those changes are not part of this change. We believe we should be able to make those changes without changes to the actual native modules from the app developers point of view.

I'll be working on getting the codegen to produce these native specs from the JS specs in a future checkin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement C++ NativeModules APIs for TurboModules

2 participants