Skip to content

feat: port style transfer implementation to C++#229

Merged
JakubGonera merged 13 commits into@jakubgonera/cppfrom
@jakubgonera/style-transfer-cpp
May 20, 2025
Merged

feat: port style transfer implementation to C++#229
JakubGonera merged 13 commits into@jakubgonera/cppfrom
@jakubgonera/style-transfer-cpp

Conversation

@JakubGonera
Copy link
Copy Markdown
Contributor

@JakubGonera JakubGonera commented Apr 29, 2025

Port native implementation of style transfer to C++ #227. This is a part of a larger effort to merge native code from Kotlin and Objective C to a single implementation in C++.

Changes by sections

Typescript

Old style transfer modules have been replaced by new, stateful implementation. For this purpose non-static versions of useModule and BaseModule have been introduced.

Objective C & Kotlin

Old implementations of style transfer have been removed. Added methods for handling fetching data via https that are passed on as function objects to C++.

C++

  • Ported image processing capabilities equivalent to the ObjC/Kotlin implementations.
  • ModelHostObject serves as an automatic interface between model implementations and JSI. This is done by template metaprogramming; defining methods in JsiConversions.h for types used by the model is necessary for it to work.
  • A factory method for loading a style transfer model is registered in RnExecutorchInstaller.

Code attribution

Dependencies introduced/updated

  • headers for Executorch 0.6 have been updated
  • OpenCV 4.11.0 dependency is introduced for Android/C++. For iOS the version is bumped via Cocoapods to 4.11.0.
  • C10 headers are the dependency of OpenCV.

Known issues

  • Exceptions in native C++ do not result in promise getting rejected when forwarding yet.
  • Garbage collection can malfunction for host objects. C++ host objects need to notify JS runtime about their size via external memory pressure for the garbage collector to know to free them. This is not yet done.

@JakubGonera JakubGonera marked this pull request as draft April 30, 2025 08:25
@chmjkb chmjkb linked an issue Apr 30, 2025 that may be closed by this pull request
@JakubGonera JakubGonera force-pushed the @jakubgonera/style-transfer-cpp branch from b593b51 to cfe8795 Compare April 30, 2025 09:09
@JakubGonera JakubGonera marked this pull request as ready for review April 30, 2025 09:21
Comment thread common/rnexecutorch/data_processing/ImageProcessing.cpp Outdated
Comment thread common/rnexecutorch/host_objects/ModelHostObject.h
Comment thread common/rnexecutorch/models/BaseModel.cpp Outdated
Comment thread common/rnexecutorch/data_processing/ImageProcessing.cpp Outdated
Comment thread common/rnexecutorch/data_processing/ImageProcessing.cpp
std::filesystem::path filePath = tempDir / filename;

if (!cv::imwrite(filePath.string(), image)) {
throw std::runtime_error("Failed to save the image: " + filePath.string());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Btw, how are those exceptions resolved on the JS side?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All of these have to be caught and currently they are then used to reject a promise -- if they are not caught we will get a runtime error that might crash the app. If the code throwing an exception is not called via a promise, but we still want the error to be pushed to JS, a JSError needs to be created and registered with the JS runtime. I could add a helper function to throw JSError with a C++ exception, the question is whether to do this in this PR. Thoughts?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

sounds like a useful helper function to me

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It turns out that it's sufficient to throw a JSError with the runtime attached to make the exception readable in JS. The only place where we call the native code without a promise is RnExecutorchInstaller, so right now we catch there any exceptions and rethrow them as JSErorrs.

Comment thread src/modules/computer_vision/StyleTransferModule.ts Outdated
Comment thread src/modules/BaseNonStaticModule.ts Outdated
Comment thread src/modules/BaseNonStaticModule.ts Outdated
Comment thread common/rnexecutorch/data_processing/ImageProcessing.cpp
Comment thread common/rnexecutorch/models/BaseModel.cpp Outdated
Comment thread common/rnexecutorch/models/StyleTransfer.cpp Outdated
Comment thread common/rnexecutorch/models/StyleTransfer.cpp Outdated
Comment thread src/hooks/useNonStaticModule.ts Outdated
modelSource
);
return global.loadStyleTransfer(loadedPaths[0] || '');
this.nativeModule = global.loadStyleTransfer(paths[0] || '');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Make sure this actually works when loading fails

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

isn't global.loadStyleTransfer possibly null?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indeed it could be. The question is whether we could do anything about this. Right now the user will have an exception thrown at startup with a message indicating this. If this is null then this means that all the native code went out the window.

Comment thread common/rnexecutorch/Log.cpp
Comment thread common/rnexecutorch/jsi/JsiHostObject.cpp Outdated
Comment thread common/rnexecutorch/data_processing/ImageProcessing.cpp
std::filesystem::path filePath = tempDir / filename;

if (!cv::imwrite(filePath.string(), image)) {
throw std::runtime_error("Failed to save the image: " + filePath.string());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

sounds like a useful helper function to me

Comment on lines +87 to +92
while (std::getline(uriStream, stringData, ',')) {
if (segmentIndex == 1)
break;
++segmentIndex;
}
if (segmentIndex != 1) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

looking at this, am I right that if uriStream is malformed so that it contains multiple comas this breaks? Shouldn't we parse whole uriStream?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

right now segmentIndex is either 0 or 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

std::getline will fail if there are two or more consecutive separators.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So that's a good point, that we need to somehow check for some malformed uris.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right now if the user supplies a string that has two commas next to each other we will pass an empty string to OpenCV which will return an empty image which in turn will result in an exception "Read image error: invalid argument" being thrown. The same holds for other cases where the payload does not represent a valid image. The exception will then result in a rejected promise.

I don't think that we should do any more work here on parsing. As far as I understand URI specification everything after the comma should be a base64 payload, and if the user is not conforming to this standard it shouldn't make any difference whether we fail while parsing a part of the intended payload or while parsing the whole.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

SGTM

Comment thread common/rnexecutorch/data_processing/ImageProcessing.cpp
Comment thread common/rnexecutorch/data_processing/base64.cpp Outdated
Comment thread common/rnexecutorch/data_processing/base64.cpp Outdated
modelSource
);
return global.loadStyleTransfer(loadedPaths[0] || '');
this.nativeModule = global.loadStyleTransfer(paths[0] || '');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

isn't global.loadStyleTransfer possibly null?

Comment thread third-party/include/executorch/runtime/core/error.h
@JakubGonera JakubGonera requested a review from mkopcins May 15, 2025 09:05
@JakubGonera JakubGonera force-pushed the @jakubgonera/style-transfer-cpp branch from c8cbe4a to a991245 Compare May 19, 2025 07:57
@JakubGonera JakubGonera merged commit 1d3224c into @jakubgonera/cpp May 20, 2025
2 checks passed
@JakubGonera JakubGonera deleted the @jakubgonera/style-transfer-cpp branch May 20, 2025 13:13
JakubGonera added a commit that referenced this pull request May 28, 2025
Port native implementation of style transfer to C++ #227. This is a part
of a larger effort to merge native code from Kotlin and Objective C to a
single implementation in C++.

Old style transfer modules have been replaced by new, stateful
implementation. For this purpose non-static versions of useModule and
BaseModule have been introduced.
Old implementations of style transfer have been removed. Added methods
for handling fetching data via https that are passed on as function
objects to C++.
- Ported image processing capabilities equivalent to the ObjC/Kotlin
implementations.
- `ModelHostObject` serves as an automatic interface between model
implementations and JSI. This is done by template metaprogramming;
defining methods in `JsiConversions.h` for types used by the model is
necessary for it to work.
- A factory method for loading a style transfer model is registered in
`RnExecutorchInstaller`.

- `common/rnexecutorch/jsi` originates from react-native-audio-api
- `base64.*` is used for converting from base64 due to
https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp/
- `ada` is a header-only library for parsing urls due to
https://github.com/ada-url/ada
- headers for Executorch 0.6 have been updated
- OpenCV 4.11.0 dependency is introduced for Android/C++. For iOS the
version is bumped via Cocoapods to 4.11.0.
- C10 headers are the dependency of OpenCV.

- Exceptions in native C++ do not result in promise getting rejected
when forwarding yet.
- Garbage collection can malfunction for host objects. C++ host objects
need to notify JS runtime about their size via external memory pressure
for the garbage collector to know to free them. This is not yet done.
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.

Port style transfer model to C++

5 participants