Making Node.js Async Function with Libuv Thread Pool

libuv is a cross-platform C library for Node.js asynchronous I/O model. It implements Node.js event loop and uses a thread pool to avoid blocking the Node.js event loop with time-consuming I/O operations. The post shares how to use libuv to optimize Dynamsoft barcode addon for Node.js.

node.js libuv barcode

Learning Resources

How to Optimize Dynamsoft Barcode Addon for Node.js with Libuv

Prerequisites

Quickly Build a Node.js C/C++ Barcode Addon

  1. Create dbr.cc with a function DecodeFile:
    #include <node.h>
    #include <node_buffer.h>
    #include <string.h>
    #include <uv.h>
    #include "If_DBR.h"
    #include "BarcodeFormat.h"
    #include "BarcodeStructs.h"
    #include "ErrorCode.h"
    
    using namespace v8;
    
    // Barcode format
    const char * GetFormatStr(__int64 format)
    {
    	if (format == CODE_39)
    		return "CODE_39";
    	if (format == CODE_128)
    		return "CODE_128";
    	if (format == CODE_93)
    		return "CODE_93";
    	if (format == CODABAR)
    		return "CODABAR";
    	if (format == ITF)
    		return "ITF";
    	if (format == UPC_A)
    		return "UPC_A";
    	if (format == UPC_E)
    		return "UPC_E";
    	if (format == EAN_13)
    		return "EAN_13";
    	if (format == EAN_8)
    		return "EAN_8";
    	if (format == INDUSTRIAL_25)
    		return "INDUSTRIAL_25";
    	if (format == QR_CODE)
    		return "QR_CODE";
    	if (format == PDF417)
    		return "PDF417";
    	if (format == DATAMATRIX)
    		return "DATAMATRIX";
    
    	return "UNKNOWN";
    }
    
    /*
     *	decodeFile(fileName, barcodeTypes, callback)
     */
    void DecodeFile(const FunctionCallbackInfo<Value>& args) {
    
    	Isolate* isolate = Isolate::GetCurrent();
    	HandleScope scope(isolate);
    
    	// get arguments
    	String::Utf8Value fileName(args[0]->ToString()); // convert v8 string to char *
    	char *pFileName = *fileName; // file name
    	__int64 llFormat = args[1]->IntegerValue();	// barcode types
    	Local<Function> cb = Local<Function>::Cast(args[2]); // javascript callback function
    
    	// initialize Dynamsoft Barcode Reader
    	int iMaxCount = 0x7FFFFFFF;
    	ReaderOptions ro = {0};
    	pBarcodeResultArray pResults = NULL;
    	ro.llBarcodeFormat = llFormat;
    	ro.iMaxBarcodesNumPerPage = iMaxCount;
    
    	// decode barcode image
    	int ret = DBR_DecodeFile(pFileName, &ro, &pResults);
    	if (ret)
    		printf("Detection error code: %d\n", ret);
    
    	int count = pResults->iBarcodeCount;
    	pBarcodeResult* ppBarcodes = pResults->ppBarcodes;
    	pBarcodeResult tmp = NULL;
    
    	// array for storing barcode results
    	Local<Array> barcodeResults = Array::New(isolate);
    
    	for (int i = 0; i < count; i++)
    	{
    		tmp = ppBarcodes[i];
    
    		Local<Object> result = Object::New(isolate);
    		result->Set(String::NewFromUtf8(isolate, "format"), String::NewFromUtf8(isolate, GetFormatStr(tmp->llFormat)));
    		result->Set(String::NewFromUtf8(isolate, "value"), String::NewFromUtf8(isolate, tmp->pBarcodeData));
    		barcodeResults->Set(Number::New(isolate, i), result);
    	}
    
    	// release memory of barcode results
    	DBR_FreeBarcodeResults(&pResults);
    
    	// run the callback
    	const unsigned argc = 1;
    	Local<Value> argv[argc] = { barcodeResults };
    	cb->Call(isolate->GetCurrentContext()->Global(), argc, argv);
    }
    
    void Init(Handle<Object> exports) {
    	NODE_SET_METHOD(exports, "decodeFile", DecodeFile);
    }
    
    NODE_MODULE(dbr, Init)
  2. Create binding.gyp. Since Dynamsoft Barcode Reader is available for Windows, Linux, and macOS, you can make configuration for all supported platforms:
    {
      "targets": [
        {
          'target_name': "dbr",
          'sources': [ "dbr.cc" ],
          'conditions': [
              ['OS=="linux"', {
                'defines': [
                  'LINUX_DBR',
                ],
                'include_dirs': [
                    "/home/xiao/Dynamsoft/BarcodeReader4.0/Include"
                ],
                'libraries': [
                    "-lDynamsoftBarcodeReaderx64", "-L/home/xiao/Dynamsoft/BarcodeReader4.0/Redist"
                ],
                'copies': [
                {
                  'destination': 'build/Release/',
                  'files': [
                    '/home/xiao/Dynamsoft/BarcodeReader4.0/Redist/libDynamsoftBarcodeReaderx64.so'
                  ]
                }]
              }],
              ['OS=="win"', {
                'defines': [
                  'WINDOWS_DBR',
                ],
                'include_dirs': [
                    "E:\Program Files (x86)\Dynamsoft\Barcode Reader 4.3\Components\C_C++\Include"
                ],
                'libraries': [
                    "-lE:\Program Files (x86)\Dynamsoft\Barcode Reader 4.3\Components\C_C++\Lib\DBRx64.lib"
                ],
                'copies': [
                {
                  'destination': 'build/Release/',
                  'files': [
                    'E:\Program Files (x86)\Dynamsoft\Barcode Reader 4.3\Components\C_C++\Redist\DynamsoftBarcodeReaderx64.dll'
                  ]
                }]
              }],
              ['OS=="mac"', {
                'defines': [
                  'MAC_DBR',
                ],
                'include_dirs' : [
                    "/Applications/Dynamsoft/Barcode\ Reader\ 4.1/Include"
                ],
                'libraries': [
                    "-lDynamsoftBarcodeReader"
                ]
              }]
          ]
        }
      ]
    }
  3. Configure build environment:
    node-gyp configure
  4. Build the project to generate the compiled dbr.node file:
    node-gyp build
  5. Use the addon in dbr.js:
    var dbr = require('./build/Release/dbr');
    var readline = require('readline');
    var fs = require('fs');
    var barcodeTypes = 0x3FF | 0x2000000 | 0x8000000 | 0x4000000; // 1D, QRCODE, PDF417, DataMatrix
    
    function decodeFile(fileName) {
        dbr.decodeFile(
            fileName, barcodeTypes,
            function(msg) {
                var result = null;
                for (index in msg) {
                    result = msg[index]
                    console.log("Format: " + result['format']);
                    console.log("Value : " + result['value']);
                    console.log("##################");
                }
            }
        );
    }
    
    var rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });
    
    rl.question("Please input a barcode image path: ", function(answer) {
        decodeFile(answer);
        rl.close();
    });

Asynchronous Function with Libuv Thread Pool

The above code only implements a synchronous interface. When barcode detection takes too much time, it apparently will block the JavaScript event loop. Therefore, we have to solve this issue with the libuv thread pool.

To send heavy work to the thread pool, we just need to call the API uv_queue_work(uv_loop_t* loop, uv_work_t* req, uv_work_cb work_cb, uv_after_work_cb after_work_cb).

Create a new function decodeFileAsync:

/*
 *	decodeFileAsync(fileName, barcodeTypes, callback)
 */
void DecodeFileAsync(const FunctionCallbackInfo<Value>& args) {
	Isolate* isolate = Isolate::GetCurrent();
	HandleScope scope(isolate);

	// get arguments
	String::Utf8Value fileName(args[0]->ToString()); // file name
	char *pFileName = *fileName;
	__int64 llFormat = args[1]->IntegerValue(); // barcode types
	Local<Function> cb = Local<Function>::Cast(args[2]); // javascript callback function

	// initialize BarcodeWorker
	BarcodeWorker *worker = new BarcodeWorker;
	worker->request.data = worker;
	strcpy(worker->filename, pFileName);
	worker->callback.Reset(isolate, cb);
	worker->llFormat = llFormat;
	worker->pResults = NULL;
	worker->buffer = NULL;
	
	uv_queue_work(uv_default_loop(), &worker->request, (uv_work_cb)DetectionWorking, (uv_after_work_cb)DetectionDone);
}

void Init(Handle<Object> exports) {
	NODE_SET_METHOD(exports, "decodeFile", DecodeFile);
	NODE_SET_METHOD(exports, "decodeFileAsync", DecodeFileAsync);
}

Move barcode detection work to a uv_work_cb callback function that executed in a worker thread:

/*
 *	uv_work_cb
 */
static void DetectionWorking(uv_work_t *req)
{
	// get the reference to BarcodeWorker
    BarcodeWorker *worker = static_cast<BarcodeWorker *>(req->data);

	// initialize Dynamsoft Barcode Reader
	int iMaxCount = 0x7FFFFFFF;
	ReaderOptions ro = {0};
	pBarcodeResultArray pResults = NULL;
	ro.llBarcodeFormat = worker->llFormat;
	ro.iMaxBarcodesNumPerPage = iMaxCount;

	// decode barcode image
	int ret = 0;
	if (worker->buffer) 
	{
		ret = DBR_DecodeStream(worker->buffer, worker->size, &ro, &pResults);
	}
	else
	{
		ret = DBR_DecodeFile(worker->filename, &ro, &pResults);
	}
	
	if (ret)
		printf("Detection error code: %d\n", ret);

	// save results to BarcodeWorker
	worker->errorCode = ret;
	worker->pResults = pResults;
}

Display barcode results in a uv_after_work_cb callback function that executed in the JavaScript thread:

/*
 *	uv_after_work_cb
 */
static void DetectionDone(uv_work_t *req,int status)
{
	Isolate* isolate = Isolate::GetCurrent();
	HandleScope scope(isolate);

    // get the reference to BarcodeWorker
    BarcodeWorker *worker = static_cast<BarcodeWorker *>(req->data);

	// get barcode results
	pBarcodeResultArray pResults = worker->pResults;
	int errorCode = worker->errorCode;
	int count = pResults->iBarcodeCount;
	pBarcodeResult* ppBarcodes = pResults->ppBarcodes;
	pBarcodeResult tmp = NULL;

	// array for storing barcode results
	Local<Array> barcodeResults = Array::New(isolate);

	for (int i = 0; i < count; i++)
	{
		tmp = ppBarcodes[i];

		Local<Object> result = Object::New(isolate);
		result->Set(String::NewFromUtf8(isolate, "format"), String::NewFromUtf8(isolate, GetFormatStr(tmp->llFormat)));
		result->Set(String::NewFromUtf8(isolate, "value"), String::NewFromUtf8(isolate, tmp->pBarcodeData));
		barcodeResults->Set(Number::New(isolate, i), result);
	}

	// release memory of barcode results
	DBR_FreeBarcodeResults(&pResults);

    // run the callback
	const unsigned argc = 1;
	Local<Value> argv[argc] = {barcodeResults};
	Local<Function> cb = Local<Function>::New(isolate, worker->callback);
    cb->Call(isolate->GetCurrentContext()->Global(), argc, argv);

	// release memory of BarcodeWorker
    delete worker;
}

Modify dbr.js and run it again:

var dbr = require('./build/Release/dbr');
var readline = require('readline');
var fs = require('fs');
var barcodeTypes = 0x3FF | 0x2000000 | 0x8000000 | 0x4000000; // 1D, QRCODE, PDF417, DataMatrix

function decodeFileAsync(fileName) {
    dbr.decodeFileAsync(
        fileName, barcodeTypes,
        function(msg) {
            var result = null;
            for (index in msg) {
                result = msg[index]
                console.log("Format: " + result['format']);
                console.log("Value : " + result['value']);
                console.log("##################");
            }
        }
    );
}

var rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

rl.question("Please input a barcode image path: ", function(answer) {
    decodeFileAsync(answer);
    rl.close();
});

Source Code

https://github.com/yushulx/nodejs-barcode-for-win-linux-mac