Now we need to write some native code for each platform our plugin supports. Our “prime” plugin supports four platforms: browser
, ios
, android
, and windows
(not WP8).
Let’s start with the browser
and windows
platforms, since that code will be the most familiar, since it’s all JavaScript.
Browser & Windows (JS) Code
The code for the browser
platform is located at src/browser/isPrimeProxy.js
. Windows code is located at src/windows/isPrimeProxy.js
. In our case, the code is identical, but this may not be so for plugins you develop.
-
First, we need to write some boilerplate code:
function isPrime(successFn, failureFn, args) { /* our algorithm will go here */ } module.exports = { isPrime: isPrime }; require("cordova/exec/proxy").add("IsPrime", module.exports);
- Notice that the
isPrime
function takes arguments in the same order as theexec
call from our public-facing API. The only difference is the missing “service” and “action” arguments. - The function is then exported using
module.exports
. - And here’s the reason why we use
Proxy
as a filename suffix: Cordova proxies requests internally so that our public-facing API stays the same for all platforms. But we have to tell Cordova how to proxy our functions, and that’s what therequire
line is doing. Note that “IsPrime” matches theSERVICE
constant in our public API.
- Notice that the
-
Next, we can write our algorithm:
function isPrime(successFn, failureFn, args) { var result = args[0], candidate = result.candidate, half = Math.floor(candidate / 2); for (var i = 2; i <= half; i++) { result.progress = ((i + 1) / half) * 100; if (candidate % i === 0) { result.factors.push(i); } if (i % 1000) { successFn(result, {keepCallback: true}); // post progress } } result.complete = true; result.progress = 100; result.isPrime = result.factors.length === 0; if (!result.isPrime) { result.factors.push(candidate); // we can divide by ourselves result.factors.unshift(1); // and by one } successFn(result); }
- Arguments to our function are in an array; we have to index it to get the results object that our public API created.
- We directly call a callback to report success or failure to our plugin’s consumer.
- Progress is reported frequently, but it calls the success callback to report the progress. To work, we need to tell Cordova to keep the callback alive using
{keepCallback: true}
. - When the calculation is complete, we don’t need to keep the callback alive, so just passing the result is sufficient.
- Progress is reported frequently, but it calls the success callback to report the progress. To work, we need to tell Cordova to keep the callback alive using
-
You could use this plugin now, but there’s a glaring problem: while the calculation is underway, the browser freezes. This is far from what we want. Let’s address that by batching up our calculation and using
setTimeout
:function isPrimeBatch(result, startAt, batchSize, endAt) { var stopAt = Math.min(startAt + batchSize - 1, endAt), candidate = result.candidate; if (candidate === 2) { return; } for (var i = startAt; i <= stopAt; i++) { if ((candidate % i) === 0) { result.factors.push(i); } } result.progress = ((i + 1) / endAt) * 100; return i + 1; } function isPrime(successFn, failureFn, args) { var result = args[0], candidate = result.candidate, half = Math.floor(candidate / 2), batchSize = 10000, cur = 2; setTimeout(function runBatch() { cur = isPrimeBatch(result, cur, batchSize, half); if (!cur || cur > half) { result.complete = true; result.progress = 100; result.isPrime = result.factors.length === 0; if (!result.isPrime) { result.factors.push(candidate); // we can divide by ourselves result.factors.unshift(1); // and by one } successFn(result); } else { successFn(result, {keepCallback: true}); // post progress setTimeout(runBatch, 0); } }, 0); }
- This method is a lot slower, but the browser remains responsive during the calculation.
- As a benefit, our progress reporting works correctly too (the prior code would never have actually issued visible progress reports).
- This method of batching is extremely naive. A better way would be to use
requestAnimationFrame
and execute as many calculations as possible in, say, 10 - 12 milliseconds. I leave this as an exercise for you.
iOS Native Code
Now we may be getting into some unfamiliar territory — especially if you aren’t familiar with Objective-C. Our file will be in src/ios/CDVIsPrime.m
.
Tip: It is the iOS convention to use three-letter prefixes when using Objective-C.
CDV
here stands for “Cordova”.
Note: You can also write plugins using Swift, but as I’m more comfortable with Objective-C, that’s what I used.
-
Let’s get the preamble out of the way:
#import <Cordova/CDV.h> @interface CDVIsPrime : CDVPlugin @end @implementation CDVIsPrime - (void)isPrime:(CDVInvokedUrlCommand*)command { } @end
- We’re defining both the interface and the implementation in a single file. You’ll often see this split out into two files: a header file (
.h
) and a module file (.m
). (Note: Swift doesn’t have this kind of distinction.) - Our plugin’s class is called
CDVIsPrime
, and it extendsCDVPlugin
(provided by Cordova). - Our methods are inside the @implementation, and named to match the “action” we use in our consumer API.
- We’re defining both the interface and the implementation in a single file. You’ll often see this split out into two files: a header file (
-
Next, we’re going to avoid the issue we had with our browser version and we’re going to do the calculation in background mode from the beginning. This is really easy to do:
- (void)isPrime:(CDVInvokedUrlCommand*)command { [self.commandDelegate runInBackground:^{ /* our algorithm goes here */ }]; }
-
We’re going to need to extract our incoming result object, and set up some local variables:
NSMutableDictionary* result = [[command argumentAtIndex: 0] mutableCopy]; NSMutableArray* factors = result[@"factors"]; int64_t candidate = [result[@"candidate"] longLongValue]; int64_t half = candidate / 2; NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; NSTimeInterval cur = now;
- The
command
passed to us contains all the arguments provided to our consumer API. We extract these by callingargumentAtIndex
. - JSON objects are passed as
NSDictionary
objects, but these are immutable by default. We need it to be mutable, so we callmutableCopy
and store the result in anNSMutableDictionary
. - The syntax
@"key"
is a peculiar quirk of Objective-C. It’s anNSString
literal. - We need an integer type that will support JavaScript’s integers —
int64_t
fits that bill.longLongValue
is used to retrieve the candidate from the JSON object. - The
NSTimeInterval
variables are so we can track how long it has been since we last reported our calculation’s progress.
- The
-
Now we can focus on our algorithm:
if (candidate == 2) { // [1] result[@"progress"] = @(100); // [5] result[@"complete"] = @(YES); // [5] result[@"isPrime"] = @(YES); } else { for (int64_t i = 2; i<=half; i++) { result[@"progress"] = @(((double)i / (double)half)*100); if ((candidate % i) == 0) { [factors addObject:@(i)]; } if (i % 1000 == 0) { cur = [[NSDate date] timeIntervalSince1970]; if (cur - now > 1) { // [6] now = cur; // [2] CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result]; // [3] [pluginResult setKeepCallbackAsBool:YES]; // [4] [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } } } result[@"progress"] = @(100); result[@"complete"] = @(YES); if (factors.count == 0) { result[@"isPrime"] = @(YES); } else { [factors insertObject:@(1) atIndex:0]; [factors addObject:@(candidate)]; } } // [2] CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result]; // [4] [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
- First, if the candidate is
2
, we just bail and say, yes, it’s prime. Otherwise the logic is identical to the browser and Windows version. - We can prepare a result by calling
[CDVPluginResult resultWithStatus: messageAsDictionary:]
(there are overloads for other types if you need them).- The status indicates if the success or failure callback will be called.
CDVCommandStatus_ERROR
will trigger the failure callback instead. - For more info, see the documentation.
- The status indicates if the success or failure callback will be called.
- Like the browser version, we have to tell Cordova to keep the callback alive if we’re not done with it yet.
- We pass the result back by calling
sendPluginResult: callbackId:
. - Confused by the
@()
? It’s shorthand for boxing a primitive into an object —NSDictionary
requires its contents to be objects. And yes, the booleans are strange (“YES” means true, and “NO” means false). - We report progress roughly every second; reporting too often slows our progress (and can make it worse than the browser version!)
- First, if the candidate is
Android Native Code
We’re using Java for our Android plugin, so the code should be pretty easy to follow. It is located under src/android/IsPrime.java
.
-
Like each platform, let’s start with the boilerplate:
package com.kerrishotts.example.isprime; import java.util.Calendar; import java.util.GregorianCalendar; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.PluginResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.annotation.SuppressLint; import android.util.Base64; public class IsPrime extends CordovaPlugin { @SuppressLint("NewApi") @Override public boolean execute(String action, final JSONArray args, final CallbackContext callbackContext) throws JSONException { if ("isPrime".equals(action)) { this.isPrime(args.getJSONObject(0), callbackContext); } else { return false; } return true; } }
- That’s a lot of imports! The calendar imports are there because we need to track time since last progress report. But in most plugins you’ll need the remaining
org.apache.cordova
imports as well as theorg.json.*
imports. - As with iOS, our class is named
IsPrime
and extends theCordovaPlugin
class provided by Cordova. - When we call
cordova.exec
in JavaScript,execute
here is what eventually is called. Unlike iOS, Android does not automatically call a method with the same name as the “action”. Instead, we have to do that work ourselves. - Notice the call to
this.isPrime()
— we’ll write that next. Of note is that we extract the arguments passed to us now. There are variousget%
methods for any types you might need. execute
expects us to returntrue
if an action is valid andfalse
if it isn’t. Thereturn
order may seem strange here, but it makes sense if you have multiple actions.
- That’s a lot of imports! The calendar imports are there because we need to track time since last progress report. But in most plugins you’ll need the remaining
-
Next we need to create the
isPrime
method we’re using in the boilerplate. Like with iOS, we’ll jump straight to using background threads.private void isPrime(final JSONObject result, final CallbackContext callbackContext) throws JSONException { cordova.getThreadPool().execute(new Runnable() { public void run() { try { /* our algorithm goes here */ } catch (JSONException e) { callbackContext.error("JSON Exception; check that the JS API is passing the right result object"); } } }); }
- There’s not much exciting here, except how we can pass errors back to JavaScript. We do this by calling
callbackContext.error()
.
- There’s not much exciting here, except how we can pass errors back to JavaScript. We do this by calling
-
We need to extract our
factors
array and set up some variables:JSONArray factors = result.getJSONArray("factors"); long candidate = result.getLong("candidate"); long half = candidate / 2; long now = (new GregorianCalendar()).getTimeInMillis(); long cur = now;
- We’re using
long
to ensure that the variables are large enough to support JavaScript’s integer size. - The
now
andcur
variables are there to track how long it has been since we’ve last reported progress. - Unlike iOS, we don’t have to worry about immutability — everything’s mutable by default.
- We’re using
-
Now we can focus on our algorithm:
if (candidate == 2) { // [1] result.put("progress", 100); result.put("complete", true); result.put("isPrime", true); } else { factors.put(1); // [2a] for (long i = 2; i<=half; i++) { if (i % 1000 == 0) { result.put("progress", ((double)i / (double)half) * 100); cur = (new GregorianCalendar()).getTimeInMillis(); if (cur - now > 1000) { // [3] now = cur; // [4] PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, result); // [5] pluginResult.setKeepCallback(true); // [6] callbackContext.sendPluginResult(pluginResult); } } if ((candidate % i) == 0) { factors.put(i); } } if (factors.length() == 1) { result.put("isPrime", true); factors.remove(0); // [2b] } else { factors.put(candidate); } } result.put("progress", 100); result.put("complete", true); // [4] PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, result); // [6] callbackContext.sendPluginResult(pluginResult);
- The algorithm is mostly the same as the iOS version. If the candidate is 2, we can bail early, otherwise we continue the calculation.
- The only real difference is that we add
1
as the first factor before determining if the number is prime or not; other versions of this would insert it at the end. Should the number be prime, this value is removed in 2b. - If it’s been more than a second since our last report, send a progress update.
- We can create a result to send back to the JavaScript side by creating a new
PluginResult
.- The status determines if the success or failure callback is triggered.
PluginResult.Status.ERROR
, along with several other status types, will call the failure callback. (See https://github.com/apache/cordova-android/blob/master/framework/src/org/apache/cordova/PluginResult.java#L186 for all the codes). callbackContext.success()
andcallbackContext.error()
are shortcuts if you don’t need to do anything to thePluginResult
… like keeping it alive.
- The status determines if the success or failure callback is triggered.
- As with iOS, we need to tell Android to keep the callback alive until we’re done. We do this by calling
pluginResult.setKeepCallback(true)
. callbackContext.sendPluginResult
is what actually sends the message back to JavaScript.
Windows Managed Code
If you were to use the Windows version of this plugin, you’d find it a little slow — it’s no better than using a straight JavaScript implementation. To do better, we’d need to write some managed code.
Writing some managed code isn’t hard, thankfully. In this example, we’ll be using C#, but you can use any language supported by the .NET runtime.
- Create a new managed component in Visual Studio 2017
- File | New | New Project…
- Expand Installed, then select Other Languages > Visual C# > Windows Universal
- Click Windows Runtime Component (Universal Windows)
- In Name, type “IsPrimeRuntimeComponent”
- In Location, browse to “plugin root\src\windows”
- Name the solution “IsPrimeRuntimeComponent”
- Click OK
- Rename
class1.cs
toIsPrimeRT.cs
- Add the following code — we’ll comment on it in a moment.
using System; using System.Collections.Generic; using System.Linq; namespace IsPrimeRuntimeComponent { public sealed class IsPrimeRT { static public long[] Batch(long candidate, long startAt, long batchSize, long endAt) { long stopAt = Math.Min(startAt + batchSize - 1, endAt); List<long> results = new List<long>(); if (candidate == 2) { return results.ToArray<long>(); } for (long i = startAt; i <= stopAt; i++) { if ((candidate % i) == 0) { results.Add(i); } } return results.ToArray<long>(); } } }
- Build a release version
- Change the solution configuration to Release
- Leave the solution platform as Any CPU
- Build | Build Solution
- Add the following to
plugin.xml
under thewindows
platform:<framework src="src/windows/IsPrimeRuntimeComponent/IsPrimeRuntimeComponent/bin/Release/IsPrimeRuntimeComponent.winmd" custom="true"/>
- Adjust
.gitignore
and.npmignore
as needed to eliminate unneeded files(see repository for examples) - Adjust the
src/windows/isPrimeProxy.js
file slightly:- Remove the
isPrimeBatch
function - Modify the first lines of the
isPrime
function:function isPrime(successFn, failureFn, args) { var result = args[0], candidate = result.candidate, half = Math.floor(candidate / 2), batchSize = 100000, // increased by factor of 10 cur = 2; setTimeout(function runBatch() { // get list of factors in this batch var results = IsPrimeRuntimeComponent.IsPrimeRT.batch(candidate, cur, batchSize, half); // [1] cur = Math.min(half + 1, cur + batchSize); if (results && results.length > 0) { // The return result isn't a real array, so use Array.from() result.factors = result.factors.concat(Array.from(results)); } // and calc progress result.progress = (cur / half) * 100; if (!cur || cur > half) { /* no further changes */
- Remove the
You may have noticed that the implementation in step 3 nearly exactly duplicates the isPrimeBatch
JavaScript function we remove in step 7. This is, after all, the majority of the hard work: dividing numbers and finding factors. The rest is rather simple work that doesn’t depend on speed. Furthermore, this method illustrates how you can mix and match JavaScript and managed code without having complex bridges between the two — JavaScript is a first-class language and can communicate directly with managed components (see 7.2, comment 1).
The only thing we’ve done is increase the batchSize
since the managed code can perform faster. Of course, in the real world, you’d try and determine the appropriate batch size rather than hard-coding it. And in the real world, you’d use a background thread as well, just like we did with iOS and Android. For an excellent example of this, see Microsoft’s excellent documentation on Threading and async programming / Submit a work item to the thread pool.
Navigation: