diff --git a/README.md b/README.md index 3e3825695..b9fdf1f75 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ If you want to create your own React Native module, scaffolding the project can - [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) and [Husky](https://github.com/typicode/husky) pre-configured - Bob pre-configured to compile your files - CircleCI pre-configured to run tests on the CI +- Use C++ code to boost performance of your modules @@ -26,6 +27,7 @@ Bob can build code for following targets: - Flow definitions (copies .js files to .flow files) - TypeScript definitions (uses `tsc` to generate declaration files) - Android AAR files +- C++ modules ## Why diff --git a/src/create.ts b/src/create.ts index de48fb5a5..5aaa72039 100644 --- a/src/create.ts +++ b/src/create.ts @@ -12,6 +12,7 @@ import pack from '../package.json'; const TEMPLATE = path.resolve(__dirname, '../templates/library'); const BINARIES = /(gradlew|\.(jar|keystore|png|jpg|gif))$/; +const CPP_FILES = path.resolve(__dirname, '../templates/cppLibrary'); export default async function create(argv: yargs.Arguments) { const folder = path.join(process.cwd(), argv.name); @@ -51,6 +52,7 @@ export default async function create(argv: yargs.Arguments) { authorEmail, authorUrl, githubUrl: repo, + useCpp, } = (await inquirer.prompt([ { type: 'input', @@ -118,6 +120,12 @@ export default async function create(argv: yargs.Arguments) { }, validate: input => /^https?:\/\//.test(input) || 'Must be a valid URL', }, + { + type: 'confirm', + name: 'useCpp', + message: 'Does your library use C++ code?', + default: false, + }, ])) as { slug: string; description: string; @@ -125,6 +133,7 @@ export default async function create(argv: yargs.Arguments) { authorEmail: string; authorUrl: string; githubUrl: string; + useCpp: boolean; }; const project = slug.replace(/^(react-native-|@[^/]+\/)/, ''); @@ -143,6 +152,7 @@ export default async function create(argv: yargs.Arguments) { .slice(1)}`, package: slug.replace(/[^a-z0-9]/g, '').toLowerCase(), podspec: slug.replace(/[^a-z0-9]+/g, '-').replace(/^-/, ''), + useCpp, }, author: { name: authorName, @@ -176,6 +186,9 @@ export default async function create(argv: yargs.Arguments) { }; await copyDir(TEMPLATE, folder); + if (options.project.useCpp) { + await copyDir(CPP_FILES, folder); + } try { await spawn.sync( diff --git a/templates/cppLibrary/android/CMakeLists.txt b/templates/cppLibrary/android/CMakeLists.txt new file mode 100644 index 000000000..b5d27b7f0 --- /dev/null +++ b/templates/cppLibrary/android/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.4.1) + +set (CMAKE_VERBOSE_MAKEFILE ON) +set (CMAKE_CXX_STANDARD 11) + +add_library(cpp + SHARED + ../cpp/example.cpp + cpp-adapter.cpp +) + +# Specifies a path to native header files. +include_directories( + ../cpp +) diff --git a/templates/cppLibrary/android/cpp-adapter.cpp b/templates/cppLibrary/android/cpp-adapter.cpp new file mode 100644 index 000000000..96adc6b33 --- /dev/null +++ b/templates/cppLibrary/android/cpp-adapter.cpp @@ -0,0 +1,8 @@ +#include +#include "example.h" + +extern "C" +JNIEXPORT jint JNICALL +Java_com_<%= project.package %>_<%= project.name %>Module_nativeMultiply(JNIEnv *env, jclass type, jint a, jint b) { + return example::multiply(a, b); +} diff --git a/templates/cppLibrary/cpp/example.cpp b/templates/cppLibrary/cpp/example.cpp new file mode 100644 index 000000000..4a7550b13 --- /dev/null +++ b/templates/cppLibrary/cpp/example.cpp @@ -0,0 +1,7 @@ +#include "example.h" + +namespace example { + int multiply(int a, int b) { + return a * b; + } +} diff --git a/templates/cppLibrary/cpp/example.h b/templates/cppLibrary/cpp/example.h new file mode 100644 index 000000000..6b81e1074 --- /dev/null +++ b/templates/cppLibrary/cpp/example.h @@ -0,0 +1,8 @@ +#ifndef EXAMPLE_H +#define EXAMPLE_H + +namespace example { + int multiply(int a, int b); +} + +#endif /* EXAMPLE_H */ diff --git a/templates/library/<%= project.podspec %>.podspec b/templates/library/<%= project.podspec %>.podspec index f1a474ba8..e27ca3f3b 100644 --- a/templates/library/<%= project.podspec %>.podspec +++ b/templates/library/<%= project.podspec %>.podspec @@ -13,7 +13,12 @@ Pod::Spec.new do |s| s.platforms = { :ios => "9.0" } s.source = { :git => "<%= repo %>.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m}" + s.source_files = "ios/**/*.{h,m,mm}" + <% if(project.useCpp==true){ %> + s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{h,cpp}" + <% } else{ %> + s.source_files = "ios/**/*.{h,m,mm}" + <% } %> s.dependency "React" end diff --git a/templates/library/android/build.gradle b/templates/library/android/build.gradle index 2dfe7f4d0..e0f9f1b8b 100644 --- a/templates/library/android/build.gradle +++ b/templates/library/android/build.gradle @@ -33,7 +33,22 @@ android { targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') versionCode 1 versionName "1.0" + <%if (project.useCpp==true) {%> + externalNativeBuild { + cmake { + cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all" + abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' + } + } + <%}%> + } + <%if (project.useCpp==true) {%> + externalNativeBuild { + cmake { + path "CMakeLists.txt" + } } + <%}%> buildTypes { release { minifyEnabled false diff --git a/templates/library/android/src/main/java/com/<%= project.package %>/<%= project.name %>Module.kt b/templates/library/android/src/main/java/com/<%= project.package %>/<%= project.name %>Module.kt index e2d583d9b..c8d8e4ca9 100644 --- a/templates/library/android/src/main/java/com/<%= project.package %>/<%= project.name %>Module.kt +++ b/templates/library/android/src/main/java/com/<%= project.package %>/<%= project.name %>Module.kt @@ -17,4 +17,23 @@ class <%= project.name %>Module(reactContext: ReactApplicationContext) : ReactCo fun getDeviceName(promise: Promise) { promise.resolve(android.os.Build.MODEL) } + <%if (project.useCpp==true) {%> + + @ReactMethod + fun multiply(a: Int, b: Int, promise: Promise) { + promise.resolve(nativeMultiply(a, b)); + } + + external fun nativeMultiply(a: Int, b: Int): Int; + + companion object + { + + // Used to load the 'native-lib' library on application startup. + init + { + System.loadLibrary("cpp") + } + } + <%}%> } diff --git a/templates/library/example/src/App.tsx b/templates/library/example/src/App.tsx index 823ed601f..d787e374c 100644 --- a/templates/library/example/src/App.tsx +++ b/templates/library/example/src/App.tsx @@ -4,14 +4,24 @@ import <%= project.name %> from '<%= project.slug %>'; export default function App() { const [deviceName, setDeviceName] = React.useState(''); + <%if (project.useCpp==true) {%> + const [value, setValue] = React.useState(); + <%}%> + React.useEffect(() => { <%= project.name %>.getDeviceName().then(setDeviceName); + <%if (project.useCpp==true) {%> + <%= project.name %>.multiply(2, 3).then(setValue); + <%}%> }, []); return ( Device name: {deviceName} + <%if (project.useCpp==true) {%> + C++ mulitply value: {value} + <%}%> ); } diff --git a/templates/library/ios/<%= project.name %>.h b/templates/library/ios/<%= project.name %>.h index aeac1a032..88fc80896 100644 --- a/templates/library/ios/<%= project.name %>.h +++ b/templates/library/ios/<%= project.name %>.h @@ -1,5 +1,13 @@ #import +<%if (project.useCpp==true) {%> +#ifdef __cplusplus + +#import "example.h" + +#endif +<%}%> + @interface <%= project.name %> : NSObject @end diff --git a/templates/library/ios/<%= project.name %>.m b/templates/library/ios/<%= project.name %>.mm similarity index 59% rename from templates/library/ios/<%= project.name %>.m rename to templates/library/ios/<%= project.name %>.mm index af2d6bf4a..761a17972 100644 --- a/templates/library/ios/<%= project.name %>.m +++ b/templates/library/ios/<%= project.name %>.mm @@ -15,4 +15,14 @@ @implementation <%= project.name %> resolve(deviceInfo.name); } +<%if (project.useCpp==true) {%> +RCT_EXPORT_METHOD(multiply:(nonnull NSNumber*)a withB:(nonnull NSNumber*)b resolver:(RCTPromiseResolveBlock)resolve +withReject:(RCTPromiseRejectBlock)reject) +{ + long result = example::multiply([a longValue], [b longValue]); + + resolve(@(result)); +} +<%}%> + @end diff --git a/templates/library/ios/<%= project.name %>.xcodeproj/project.pbxproj b/templates/library/ios/<%= project.name %>.xcodeproj/project.pbxproj index 01fe74036..b3e0b6a58 100644 --- a/templates/library/ios/<%= project.name %>.xcodeproj/project.pbxproj +++ b/templates/library/ios/<%= project.name %>.xcodeproj/project.pbxproj @@ -7,7 +7,10 @@ objects = { /* Begin PBXBuildFile section */ - B3E7B58A1CC2AC0600A0062D /* <%= project.name %>.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* <%= project.name %>.m */; }; +<%if (project.useCpp==true) {%> + 5E46D8CD2428F78900513E24 /* example.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5E46D8CB2428F78900513E24 /* example.cpp */; }; +<%}%> + 5E555C0D2413F4C50049A1A2 /* <%= project.name %>.mm in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* <%= project.name %>.mm */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -24,8 +27,12 @@ /* Begin PBXFileReference section */ 134814201AA4EA6300B7C361 /* lib<%= project.name %>.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = lib<%= project.name %>.a; sourceTree = BUILT_PRODUCTS_DIR; }; +<%if (project.useCpp==true) {%> + 5E46D8CB2428F78900513E24 /* example.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = example.cpp; path = ../cpp/example.cpp; sourceTree = ""; }; + 5E46D8CC2428F78900513E24 /* example.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = example.h; path = ../cpp/example.h; sourceTree = ""; }; +<%}%> B3E7B5881CC2AC0600A0062D /* <%= project.name %>.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = <%= project.name %>.h; sourceTree = ""; }; - B3E7B5891CC2AC0600A0062D /* <%= project.name %>.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = <%= project.name %>.m; sourceTree = ""; }; + B3E7B5891CC2AC0600A0062D /* <%= project.name %>.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = <%= project.name %>.mm; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -50,8 +57,12 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( +<%if (project.useCpp==true) {%> + 5E46D8CB2428F78900513E24 /* example.cpp */, + 5E46D8CC2428F78900513E24 /* example.h */, +<%}%> B3E7B5881CC2AC0600A0062D /* <%= project.name %>.h */, - B3E7B5891CC2AC0600A0062D /* <%= project.name %>.m */, + B3E7B5891CC2AC0600A0062D /* <%= project.name %>.mm */, 134814211AA4EA7D00B7C361 /* Products */, ); sourceTree = ""; @@ -95,6 +106,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 58B511D21A9E6C8500147676; @@ -112,7 +124,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - B3E7B58A1CC2AC0600A0062D /* <%= project.name %>.m in Sources */, +<%if (project.useCpp==true) {%> + 5E46D8CD2428F78900513E24 /* example.cpp in Sources */, +<%}%> + 5E555C0D2413F4C50049A1A2 /* <%= project.name %>.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -216,7 +231,7 @@ isa = XCBuildConfiguration; buildSettings = { HEADER_SEARCH_PATHS = ( - "$(inherited)", + "$(inherited)", /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../../React/**", "$(SRCROOT)/../../react-native/React/**", diff --git a/templates/library/src/index.tsx b/templates/library/src/index.tsx index 13dc979d3..ea788ba8b 100644 --- a/templates/library/src/index.tsx +++ b/templates/library/src/index.tsx @@ -2,6 +2,9 @@ import { NativeModules } from 'react-native'; type <%= project.name %>Type = { getDeviceName(): Promise; + <%if (project.useCpp==true) {%> + multiply(a: number, b: number): Promise; + <%}%> }; const { <%= project.name %> } = NativeModules; diff --git a/yarn.lock b/yarn.lock index 77f764ac1..cfab0ae2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2283,6 +2283,13 @@ debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" +debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + decamelize-keys@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -5338,11 +5345,6 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== - safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"