Skip to content

Commit

Permalink
Merge pull request #51 from ashvardanian/25-nodejs-bindings-and-an-np…
Browse files Browse the repository at this point in the history
…m-package

NodeJS bindings
  • Loading branch information
ashvardanian authored Oct 5, 2023
2 parents 87f72bd + 41ada28 commit 014257c
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 25 deletions.
1 change: 0 additions & 1 deletion .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ AllowShortFunctionsOnASingleLine: true
AllowShortLambdasOnASingleLine: true
AllowShortLoopsOnASingleLine: true
AlwaysBreakTemplateDeclarations: Yes
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakAfterReturnType: None
PenaltyReturnTypeOnItsOwnLine: 200

Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/javascript-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI
on:
pull_request:
branches: '*'
push:
branches: '*'

jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
- run: npm i
- run: npm test
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
"__verbose_abort": "cpp",
"strstream": "cpp",
"filesystem": "cpp",
"stringzilla.h": "c",
"__memory": "c"
},
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",
Expand Down
128 changes: 115 additions & 13 deletions javascript/lib.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*
* @see NodeJS docs: https://nodejs.org/api/n-api.html
*/

#include <node_api.h>
#include <stringzilla.h>

Expand All @@ -17,20 +18,20 @@ napi_value FindAPI(napi_env env, napi_callback_info info) {
napi_get_cb_info(env, info, &argc, args, NULL, NULL);

// Extract the C string from the JavaScript string for haystack and needle
size_t str_size;
size_t str_len;
struct strzl_haystack_t strzl_haystack = {NULL, 0};
struct strzl_needle_t strzl_needle = {NULL, 0, 0};

// For haystack
napi_get_value_string_utf8(env, args[0], NULL, 0, &str_size);
char *haystack = malloc(str_size + 1);
napi_get_value_string_utf8(env, args[0], haystack, str_size + 1, &str_len);
struct strzl_haystack_t strzl_haystack = {haystack, str_len};
napi_get_value_string_utf8(env, args[0], NULL, 0, &strzl_haystack.len);
char *haystack = malloc(strzl_haystack.len);
napi_get_value_string_utf8(env, args[0], haystack, strzl_haystack.len, &strzl_haystack.len);
strzl_haystack.ptr = haystack;

// For needle
napi_get_value_string_utf8(env, args[1], NULL, 0, &str_size);
char *needle = malloc(str_size + 1);
napi_get_value_string_utf8(env, args[1], needle, str_size + 1, &str_len);
struct strzl_needle_t strzl_needle = {needle, str_len, 0};
napi_get_value_string_utf8(env, args[1], NULL, 0, &strzl_needle.len);
char *needle = malloc(strzl_needle.len);
napi_get_value_string_utf8(env, args[1], needle, strzl_needle.len, &strzl_needle.len);
strzl_needle.ptr = needle;

// Perform the find operation
#if defined(__AVX2__)
Expand All @@ -45,16 +46,117 @@ napi_value FindAPI(napi_env env, napi_callback_info info) {
free(haystack);
free(needle);

// Convert result to JavaScript BigInt and return
// Convert the result to JavaScript BigInt and return
napi_value js_result;

// In JavaScript, if `find` is unable to find the specified value, then it should return -1
if (result == strzl_haystack.len)
napi_create_bigint_int64(env, -1, &js_result);
else
napi_create_bigint_uint64(env, result, &js_result);

return js_result;
}

size_t count_char(strzl_haystack_t strzl_haystack, char needle) {
size_t result = strzl_naive_count_char(strzl_haystack, needle);

return result;
}

napi_value CountAPI(napi_env env, napi_callback_info info) {
size_t argc = 3;
napi_value args[3];
napi_get_cb_info(env, info, &argc, args, NULL, NULL);

// Extract the C string from the JavaScript string for haystack and needle
struct strzl_haystack_t strzl_haystack = {NULL, 0};
struct strzl_needle_t strzl_needle = {NULL, 0, 0};

// For haystack
napi_get_value_string_utf8(env, args[0], NULL, 0, &strzl_haystack.len);
char *haystack = malloc(strzl_haystack.len);
napi_get_value_string_utf8(env, args[0], haystack, strzl_haystack.len, &strzl_haystack.len);
strzl_haystack.ptr = haystack;

// For needle
napi_get_value_string_utf8(env, args[1], NULL, 0, &strzl_needle.len);
char *needle = malloc(strzl_needle.len);
napi_get_value_string_utf8(env, args[1], needle, strzl_needle.len, &strzl_needle.len);
strzl_needle.ptr = needle;

bool overlap = false;
if (argc > 2) {
napi_get_value_bool(env, args[2], &overlap);
}

size_t result;

if (strzl_needle.len == 0 || strzl_haystack.len == 0 || strzl_haystack.len < strzl_needle.len)
result = 0;
else if (strzl_needle.len == 1)
result = count_char(strzl_haystack, strzl_needle.ptr[0]);
else if (overlap) {
while (strzl_haystack.len) {
#if defined(__AVX2__)
size_t offset = strzl_avx2_find_substr(strzl_haystack, strzl_needle);
#elif defined(__ARM_NEON)
size_t offset = strzl_neon_find_substr(strzl_haystack, strzl_needle);
#else
size_t offset = strzl_naive_find_substr(strzl_haystack, strzl_needle);
#endif

bool found = offset != strzl_haystack.len;
result += found;
strzl_haystack.ptr += offset + found;
strzl_haystack.len -= offset + found;
}
}

else {
while (strzl_haystack.len) {
#if defined(__AVX2__)
size_t offset = strzl_avx2_find_substr(strzl_haystack, strzl_needle);
#elif defined(__ARM_NEON)
size_t offset = strzl_neon_find_substr(strzl_haystack, strzl_needle);
#else
size_t offset = strzl_naive_find_substr(strzl_haystack, strzl_needle);
#endif

bool found = offset != strzl_haystack.len;
result += found;
strzl_haystack.ptr += offset + strzl_needle.len;
strzl_haystack.len -= offset + strzl_needle.len * found;
}
}

// Cleanup
free(haystack);
free(needle);

// Convert the result to JavaScript `BigInt` and return
napi_value js_result;
napi_create_bigint_uint64(env, result, &js_result);

return js_result;
}

napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc = {"find", 0, FindAPI, 0, 0, 0, napi_default, 0};
napi_define_properties(env, exports, 1, &desc);
// Define the "find" property
napi_property_descriptor findDesc = {"find", 0, FindAPI, 0, 0, 0, napi_default, 0};

// Define the "count" property
napi_property_descriptor countDesc = {"count", 0, CountAPI, 0, 0, 0, napi_default, 0};

// Define an array of property descriptors
napi_property_descriptor properties[] = {findDesc, countDesc};

// Define the number of properties in the array
size_t propertyCount = sizeof(properties) / sizeof(properties[0]);

// Define the properties on the `exports` object
napi_define_properties(env, exports, propertyCount, properties);

return exports;
}

Expand Down
10 changes: 9 additions & 1 deletion javascript/stringzilla.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,12 @@
* @param {string} needle
*/
export function find(haystack: string, needle: string): bigint;


/**
* Searches for a substring in a larger string.
*
* @param {string} haystack
* @param {string} needle
* @param {boolean} overlap
*/
export function count(haystack: string, needle: string, overlap: boolean): bigint;
7 changes: 0 additions & 7 deletions javascript/test.js

This file was deleted.

44 changes: 44 additions & 0 deletions javascript/test/count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import test from 'node:test';
import bindings from 'bindings';
import assert from 'node:assert';

const stringzilla = bindings('stringzilla');

test('Count Words - Single Occurrence', () => {
const result = stringzilla.count('hello world', 'world');

assert.strictEqual(result, 1n);
});

test('Count Words - Multiple Occurrence', () => {
const result = stringzilla.count('hello world, hello John', 'hello');

assert.strictEqual(result, 2n);
});

test('Count Words - Multiple Occurrences with Overlap Test', () => {
const result_1 = stringzilla.count('abababab', 'aba');

assert.strictEqual(result_1, 2n);

const result_2 = stringzilla.count('abababab', 'aba', true);

assert.strictEqual(result_2, 3n);
});

test('Count Words - No Occurrence', () => {
const result = stringzilla.count('hello world', 'hi');

assert.strictEqual(result, 0n);
});

test('Count Words - Empty String Inputs', () => {
const result_1 = stringzilla.count('hello world', '');
assert.strictEqual(result_1, 0n);

const result_2 = stringzilla.count('', 'hi');
assert.strictEqual(result_2, 0n);

const result_3 = stringzilla.count('', '');
assert.strictEqual(result_3, 0n);
});
30 changes: 30 additions & 0 deletions javascript/test/find.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import test from 'node:test';
import bindings from 'bindings';
import assert from 'node:assert';

const stringzilla = bindings('stringzilla');

test('Find Word in Text - Positive Case', () => {
const result = stringzilla.find('hello world, hello john', 'hello');

assert.strictEqual(result, 0n);
});

test('Find Word in Text - Negative Case (Word Not Found)', () => {
const result_1 = stringzilla.find('ha', 'aaa');
assert.strictEqual(result_1, -1n);

const result_2 = stringzilla.find('g', 'a');
assert.strictEqual(result_2, -1n);
});

test('Find Word in Text - Negative Case (Empty String Inputs)', () => {
const result_1 = stringzilla.find('hello world', '');
assert.strictEqual(result_1, 0n);

const result_2 = stringzilla.find('', 'a');
assert.strictEqual(result_2, -1n);

const result_3 = stringzilla.find('', '');
assert.strictEqual(result_2, -1n);
});
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"author": "Ash Vardanian",
"license": "Apache 2.0",
"main": "javascript/stringzilla.js",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/ashvardanian/stringzilla.git"
Expand All @@ -19,7 +20,7 @@
"node-addon-api": "^3.0.0"
},
"scripts": {
"test": "node javascript/test.js"
"test": "node --test ./javascript/test"
},
"devDependencies": {
"@semantic-release/exec": "^6.0.3",
Expand Down

0 comments on commit 014257c

Please sign in to comment.