Skip to content

Commit 49bffd3

Browse files
author
Carol Hansen
authored
Add async standalone function example (#46)
* Add async standalone example
1 parent 74918ab commit 49bffd3

File tree

7 files changed

+231
-4
lines changed

7 files changed

+231
-4
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@ var check = module.hello();
7474
console.log(check); // => world
7575
```
7676

77+
### Standalone async function
78+
```javascript
79+
var module = require('./path/to/lib/index.js');
80+
81+
module.hello_async({ louder: true }, function(err, result) {
82+
if (err) throw err;
83+
console.log(hi); // => ...threads are busy async bees...world!!!!
84+
});
85+
```
86+
7787
### Object
7888
```javascript
7989
var module = require('./path/to/lib/index.js');
@@ -87,7 +97,7 @@ console.log(hi); // => howdy world!
8797
`node-cpp-skel` was designed to make adding custom code simple and scalable, to form to whatever use-case you may need. Here's how!
8898

8999
- Create a dir in `./src` to hold your custom code. See `./src/standalone` as an example.
90-
- Add your new method or class to `./src/module.cpp`
100+
- Add your new method or class to `./src/module.cpp`, and `#include` it at the top
91101
- Add your new file-to-be-compiled to the list of target sources in `./binding.gyp`
92102

93103
# Publishing Binaries

binding.gyp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
'sources': [
3737
'./src/module.cpp',
3838
'./src/standalone/hello.cpp',
39+
'./src/standalone_async/hello_async.cpp',
3940
'./src/hello_world.cpp'
4041
],
4142
'include_dirs': [

src/module.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include <nan.h>
22
#include "standalone/hello.hpp"
3+
#include "standalone_async/hello_async.hpp"
34
#include "hello_world.hpp"
5+
// #include "your_code.hpp"
46

57
// "target" is a magic var that nodejs passes into modules scope
68
// When you write things to target, they become available to call from js
@@ -9,10 +11,13 @@ static void init(v8::Local<v8::Object> target) {
911
// expose hello method
1012
Nan::SetMethod(target, "hello", standalone::hello);
1113

14+
// expose hello_async method
15+
Nan::SetMethod(target, "hello_async", standalone_async::hello_async);
16+
1217
// expose hello_world class
1318
HelloWorld::Init(target);
1419

15-
// add more methods below that youd like to use in node.js-world
20+
// add more methods/classes below that youd like to use in node.js-world
1621
// then create a .cpp and .hpp file in /src for each new method
1722
}
1823

src/standalone/hello.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace standalone {
44

5-
// "hello" is a Standalone function because it's not a class.
5+
// hello is a "standalone function" because it's not a class.
66
// If this function was not defined within a namespace, it would be in the global scope.
77
NAN_METHOD(hello) {
88

@@ -11,4 +11,4 @@ namespace standalone {
1111
info.GetReturnValue().Set(Nan::New<v8::String>("world").ToLocalChecked());
1212

1313
}
14-
}
14+
} // namespace standalone
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#include "hello_async.hpp"
2+
3+
#include <exception>
4+
#include <stdexcept>
5+
#include <iostream>
6+
#include <map>
7+
8+
namespace standalone_async {
9+
10+
/*
11+
* This is an internal function used to return callback error messages instead of
12+
* throwing errors.
13+
* Usage:
14+
*
15+
* v8::Local<v8::Function> callback;
16+
* CallbackError("error message", callback);
17+
* return; // this is important to prevent duplicate callbacks from being fired!
18+
*/
19+
inline void CallbackError(std::string message, v8::Local<v8::Function> callback) {
20+
v8::Local<v8::Value> argv[1] = { Nan::Error(message.c_str()) };
21+
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), callback, 1, argv);
22+
}
23+
24+
// Expensive allocation of std::map, querying, and string comparison,
25+
// therefore threads are busy
26+
std::string do_expensive_work(bool louder) {
27+
28+
std::map<std::size_t,std::string> container;
29+
std::size_t work_to_do=100000;
30+
31+
for (std::size_t i=0;i<work_to_do;++i) {
32+
container.emplace(i,std::to_string(i));
33+
}
34+
35+
for (std::size_t i=0;i<work_to_do;++i) {
36+
std::string const& item = container[i];
37+
if (item != std::to_string(i)) {
38+
39+
// AsyncHelloWorker's Execute function will take care of this error
40+
// and return it to js-world via callback
41+
throw std::runtime_error("Uh oh, this should never happen");
42+
}
43+
}
44+
45+
std::string result = "...threads are busy async bees...world";
46+
47+
if (louder)
48+
{
49+
result += "!!!!";
50+
}
51+
52+
return result;
53+
54+
}
55+
56+
// This is the worker running asynchronously and calling a user-provided callback when done.
57+
// Consider storing all C++ objects you need by value or by shared_ptr to keep them alive until done.
58+
// Nan AsyncWorker docs: https://github.com/nodejs/nan/blob/master/doc/asyncworker.md
59+
struct AsyncHelloWorker : Nan::AsyncWorker {
60+
using Base = Nan::AsyncWorker;
61+
62+
AsyncHelloWorker(bool louder, Nan::Callback* callback)
63+
: Base(callback), result_{""}, louder_{louder} { }
64+
65+
// The Execute() function is getting called when the worker starts to run.
66+
// - You only have access to member variables stored in this worker.
67+
// - You do not have access to Javascript v8 objects here.
68+
void Execute() override {
69+
try {
70+
result_ = do_expensive_work(louder_);
71+
} catch (const std::exception& e) {
72+
SetErrorMessage(e.what());
73+
}
74+
}
75+
76+
// The HandleOKCallback() is getting called when Execute() successfully completed.
77+
// - In case Execute() invoked SetErrorMessage("") this function is not getting called.
78+
// - You have access to Javascript v8 objects again
79+
// - You have to translate from C++ member variables to Javascript v8 objects
80+
// - Finally, you call the user's callback with your results
81+
void HandleOKCallback() override {
82+
Nan::HandleScope scope;
83+
84+
const auto argc = 2u;
85+
v8::Local<v8::Value> argv[argc] = {Nan::Null(), Nan::New<v8::String>(result_).ToLocalChecked()};
86+
87+
callback->Call(argc, argv);
88+
}
89+
90+
std::string result_;
91+
const bool louder_;
92+
};
93+
94+
// hello_async is a "standalone function" because it's not a class.
95+
// If this function was not defined within a namespace ("standalone_async"), it would be in the global scope.
96+
NAN_METHOD(hello_async) {
97+
98+
bool louder = false;
99+
100+
// Check second argument, should be a 'callback' function.
101+
// This allows us to set the callback so we can use it to return errors instead of throwing.
102+
// Also, "info" comes from the NAN_METHOD macro, which returns differently according to the version of node
103+
if (!info[1]->IsFunction())
104+
{
105+
return Nan::ThrowTypeError("second arg 'callback' must be a function");
106+
}
107+
v8::Local<v8::Function> callback = info[1].As<v8::Function>();
108+
109+
// Check first argument, should be an 'options' object
110+
if (!info[0]->IsObject())
111+
{
112+
return CallbackError("first arg 'options' must be an object", callback);
113+
}
114+
v8::Local<v8::Object> options = info[0].As<v8::Object>();
115+
116+
// Check options object for the "louder" property, which should be a boolean value
117+
if (options->Has(Nan::New("louder").ToLocalChecked()))
118+
{
119+
v8::Local<v8::Value> louder_val = options->Get(Nan::New("louder").ToLocalChecked());
120+
if (!louder_val->IsBoolean())
121+
{
122+
return CallbackError("option 'louder' must be a boolean", callback);
123+
}
124+
louder = louder_val->BooleanValue();
125+
}
126+
127+
// Create a worker instance and queues it to run asynchronously invoking the callback when done.
128+
// - Nan::AsyncWorker takes a pointer to a Nan::Callback and deletes the pointer automatically.
129+
// - Nan::AsyncQueueWorker takes a pointer to a Nan::AsyncWorker and deletes the pointer automatically.
130+
auto* worker = new AsyncHelloWorker{louder, new Nan::Callback{callback}};
131+
Nan::AsyncQueueWorker(worker);
132+
133+
}
134+
135+
} // namespace standalone_async
136+
137+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#pragma once
2+
#include <nan.h>
3+
// carrots, ex: <nan.h> --> look for header in global
4+
// quotes, ex: "hello.hpp" --> look for header in location relative to this file
5+
6+
/*
7+
8+
Namespace is an organizational method that helps to clearly show where a method is coming from.
9+
Namespaces are generally a great idea in C++ because they help us scale things. C++ has no notion of scoped modules,
10+
so all of the code you write in any file could potentially conflict with other classes/functions/etc.
11+
Namespaces help to differentiate pieces of your code.
12+
13+
The convention In this skeleton is to name the namespace to match the name of the subdirectory where it lives.
14+
So in this case, the namespace is called "standalone" because this method lives within the "standalone" subdirectory.
15+
If there is another "hello" function used for another example, the compiler will know the difference between the two:
16+
17+
standalone::hello
18+
19+
VS
20+
21+
potato::hello
22+
23+
*/
24+
namespace standalone_async {
25+
26+
// hello, custom sync method tied to module.cpp
27+
// method's logic lives in hello.cpp
28+
NAN_METHOD(hello_async);
29+
30+
}

test/hello_async.test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
var test = require('tape');
2+
var module = require('../lib/index.js');
3+
4+
test('success: prints loud busy world', function(t) {
5+
module.hello_async({ louder: true }, function(err, result) {
6+
if (err) throw err;
7+
t.equal(result, '...threads are busy async bees...world!!!!');
8+
t.end();
9+
});
10+
});
11+
12+
test('success: prints regular busy world', function(t) {
13+
module.hello_async({ louder: false }, function(err, result) {
14+
if (err) throw err;
15+
t.equal(result, '...threads are busy async bees...world');
16+
t.end();
17+
});
18+
});
19+
20+
test('fail: handles invalid louder value', function(t) {
21+
module.hello_async({ louder: 'oops' }, function(err, result) {
22+
t.ok(err, 'expected error');
23+
t.ok(err.message.indexOf('option \'louder\' must be a boolean') > -1, 'expected error message');
24+
t.end();
25+
});
26+
});
27+
28+
test('fail: handles invalid options value', function(t) {
29+
module.hello_async('oops', function(err, result) {
30+
t.ok(err, 'expected error');
31+
t.ok(err.message.indexOf('first arg \'options\' must be an object') > -1, 'expected error message');
32+
t.end();
33+
});
34+
});
35+
36+
test('fail: handles missing callback', function(t) {
37+
try {
38+
module.hello_async({ louder: 'oops' }, {});
39+
} catch (err) {
40+
t.ok(err, 'expected error');
41+
t.ok(err.message.indexOf('callback') > -1, 'proper error message');
42+
t.end();
43+
}
44+
});

0 commit comments

Comments
 (0)