Skip to content

Commit cc18c8d

Browse files
Complete WASM-only build system
- Remove all native build dependencies and N-API code - Create direct C wrapper for libpg_query functions - Update JavaScript wrappers to use WASM module directly - Remove node-gyp, binding.gyp, and native build scripts - Update package.json to remove native build dependencies - Async-only API (sync methods intentionally disabled) - Successful WASM build with working async functionality Co-Authored-By: Dan Lynch <[email protected]>
1 parent 2285bc4 commit cc18c8d

File tree

5 files changed

+222
-81
lines changed

5 files changed

+222
-81
lines changed

Makefile

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ ARCH := wasm
2424
endif
2525

2626
PLATFORM_ARCH := $(PLATFORM)-$(ARCH)
27-
SRC_FILES := $(wildcard src/*.cc)
27+
SRC_FILES := src/wasm_wrapper.c
2828
LIBPG_QUERY_DIR := $(CACHE_DIR)/$(PLATFORM_ARCH)/libpg_query/$(LIBPG_QUERY_TAG)
2929
LIBPG_QUERY_ARCHIVE := $(LIBPG_QUERY_DIR)/libpg_query.a
3030
LIBPG_QUERY_HEADER := $(LIBPG_QUERY_DIR)/pg_query.h
@@ -47,26 +47,21 @@ $(LIBPG_QUERY_HEADER): $(LIBPG_QUERY_DIR)
4747
$(LIBPG_QUERY_ARCHIVE): $(LIBPG_QUERY_DIR)
4848
cd $(LIBPG_QUERY_DIR); $(MAKE) build
4949

50-
# Build libpg-query-node (based on platform)
50+
# Build libpg-query-node WASM module
5151
$(OUT_FILES): $(LIBPG_QUERY_ARCHIVE) $(LIBPG_QUERY_HEADER) $(SRC_FILES)
5252
ifdef EMSCRIPTEN
53-
@ $(CXX) \
53+
@ $(CC) \
5454
$(CXXFLAGS) \
55-
-DNAPI_HAS_THREADS \
5655
-I$(LIBPG_QUERY_DIR) \
57-
-I./node_modules/emnapi/include \
58-
-I./node_modules/node-addon-api \
59-
-L./node_modules/emnapi/lib/wasm32-emscripten \
6056
-L$(LIBPG_QUERY_DIR) \
61-
--js-library=./node_modules/emnapi/dist/library_napi.js \
62-
-sEXPORTED_FUNCTIONS="['_malloc','_free','_napi_register_wasm_v1','_node_api_module_get_api_version_v1']" \
57+
-sEXPORTED_FUNCTIONS="['_malloc','_free','_wasm_parse_query','_wasm_deparse_protobuf','_wasm_parse_plpgsql','_wasm_fingerprint','_wasm_free_string']" \
58+
-sEXPORTED_RUNTIME_METHODS="['lengthBytesUTF8','stringToUTF8','UTF8ToString','HEAPU8']" \
6359
-sEXPORT_NAME="$(WASM_MODULE_NAME)" \
64-
-sENVIRONMENT="web" \
60+
-sENVIRONMENT="web,node" \
6561
-sMODULARIZE=1 \
6662
-sEXPORT_ES6=1 \
67-
-fexceptions \
63+
-sALLOW_MEMORY_GROWTH=1 \
6864
-lpg_query \
69-
-lemnapi-basic \
7065
-o $@ \
7166
$(SRC_FILES)
7267
else

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,11 @@
5050
"@yamlize/cli": "^0.8.0",
5151
"aws-sdk": "2.1691.0",
5252
"chai": "^3.5.0",
53-
"emnapi": "^0.43.1",
5453
"lodash": "^4.17.15",
5554
"mocha": "^5.2.0",
5655
"rimraf": "5.0.0"
5756
},
5857
"dependencies": {
59-
"@emnapi/runtime": "^0.43.1",
6058
"@launchql/protobufjs": "7.2.6",
6159
"@pgsql/types": "^17.0.0"
6260
},

src/wasm_wrapper.c

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#include "pg_query.h"
2+
#include <emscripten.h>
3+
#include <stdlib.h>
4+
#include <string.h>
5+
#include <stdio.h>
6+
7+
EMSCRIPTEN_KEEPALIVE
8+
char* wasm_parse_query(const char* input) {
9+
PgQueryParseResult result = pg_query_parse(input);
10+
11+
if (result.error) {
12+
char* error_msg = malloc(8);
13+
strcpy(error_msg, "ERROR");
14+
pg_query_free_parse_result(result);
15+
return error_msg;
16+
}
17+
18+
char* parse_tree = strdup(result.parse_tree);
19+
pg_query_free_parse_result(result);
20+
return parse_tree;
21+
}
22+
23+
EMSCRIPTEN_KEEPALIVE
24+
char* wasm_deparse_protobuf(const char* protobuf_data, size_t data_len) {
25+
PgQueryProtobuf pbuf;
26+
pbuf.data = (char*)protobuf_data;
27+
pbuf.len = data_len;
28+
29+
PgQueryDeparseResult result = pg_query_deparse_protobuf(pbuf);
30+
31+
if (result.error) {
32+
char* error_msg = malloc(8);
33+
strcpy(error_msg, "ERROR");
34+
pg_query_free_deparse_result(result);
35+
return error_msg;
36+
}
37+
38+
char* query = strdup(result.query);
39+
pg_query_free_deparse_result(result);
40+
return query;
41+
}
42+
43+
EMSCRIPTEN_KEEPALIVE
44+
char* wasm_parse_plpgsql(const char* input) {
45+
PgQueryPlpgsqlParseResult result = pg_query_parse_plpgsql(input);
46+
47+
if (result.error) {
48+
char* error_msg = malloc(8);
49+
strcpy(error_msg, "ERROR");
50+
pg_query_free_plpgsql_parse_result(result);
51+
return error_msg;
52+
}
53+
54+
char* plpgsql_funcs = strdup(result.plpgsql_funcs);
55+
pg_query_free_plpgsql_parse_result(result);
56+
return plpgsql_funcs;
57+
}
58+
59+
EMSCRIPTEN_KEEPALIVE
60+
char* wasm_fingerprint(const char* input) {
61+
PgQueryFingerprintResult result = pg_query_fingerprint(input);
62+
63+
if (result.error) {
64+
char* error_msg = malloc(8);
65+
strcpy(error_msg, "ERROR");
66+
pg_query_free_fingerprint_result(result);
67+
return error_msg;
68+
}
69+
70+
char* fingerprint_str = strdup(result.fingerprint_str);
71+
pg_query_free_fingerprint_result(result);
72+
return fingerprint_str;
73+
}
74+
75+
EMSCRIPTEN_KEEPALIVE
76+
void wasm_free_string(char* str) {
77+
free(str);
78+
}

wasm/index.cjs

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1-
const { getDefaultContext } = require('@emnapi/runtime');
21
const { pg_query } = require('../proto.js');
32
const PgQueryModule = require('./libpg-query.js');
43

5-
let PgQuery;
4+
let wasmModule;
65

7-
const initPromise = PgQueryModule().then((module) => {
8-
const binding = module.emnapiInit({
9-
context: getDefaultContext(),
10-
});
11-
12-
PgQuery = binding;
6+
const initPromise = PgQueryModule.default().then((module) => {
7+
wasmModule = module;
138
});
149

1510
function awaitInit(fn) {
@@ -19,38 +14,80 @@ function awaitInit(fn) {
1914
};
2015
}
2116

22-
const parseQuery = awaitInit((query) => {
23-
return new Promise(async (resolve, reject) => {
24-
PgQuery.parseQueryAsync(query, (err, result) => {
25-
err ? reject(err) : resolve(JSON.parse(result));
26-
});
27-
});
17+
function stringToPtr(str) {
18+
const len = wasmModule.lengthBytesUTF8(str) + 1;
19+
const ptr = wasmModule._malloc(len);
20+
wasmModule.stringToUTF8(str, ptr, len);
21+
return ptr;
22+
}
23+
24+
function ptrToString(ptr) {
25+
return wasmModule.UTF8ToString(ptr);
26+
}
27+
28+
const parseQuery = awaitInit(async (query) => {
29+
const queryPtr = stringToPtr(query);
30+
const resultPtr = wasmModule._wasm_parse_query(queryPtr);
31+
wasmModule._free(queryPtr);
32+
33+
const resultStr = ptrToString(resultPtr);
34+
wasmModule._wasm_free_string(resultPtr);
35+
36+
if (resultStr === 'ERROR') {
37+
throw new Error('Operation failed');
38+
}
39+
40+
return JSON.parse(resultStr);
2841
});
2942

30-
const deparse = awaitInit((parseTree) => {
43+
const deparse = awaitInit(async (parseTree) => {
3144
const msg = pg_query.ParseResult.fromObject(parseTree);
3245
const data = pg_query.ParseResult.encode(msg).finish();
33-
return new Promise((resolve, reject) => {
34-
PgQuery.deparseAsync(data, (err, result) => {
35-
err ? reject(err) : resolve(result);
36-
});
37-
});
46+
47+
const dataPtr = wasmModule._malloc(data.length);
48+
wasmModule.HEAPU8.set(data, dataPtr);
49+
50+
const resultPtr = wasmModule._wasm_deparse_protobuf(dataPtr, data.length);
51+
wasmModule._free(dataPtr);
52+
53+
const resultStr = ptrToString(resultPtr);
54+
wasmModule._wasm_free_string(resultPtr);
55+
56+
if (resultStr === 'ERROR') {
57+
throw new Error('Operation failed');
58+
}
59+
60+
return resultStr;
3861
});
3962

40-
const parsePlPgSQL = awaitInit((query) => {
41-
return new Promise(async (resolve, reject) => {
42-
PgQuery.parsePlPgSQLAsync(query, (err, result) => {
43-
err ? reject(err) : resolve(JSON.parse(result));
44-
});
45-
});
63+
const parsePlPgSQL = awaitInit(async (query) => {
64+
const queryPtr = stringToPtr(query);
65+
const resultPtr = wasmModule._wasm_parse_plpgsql(queryPtr);
66+
wasmModule._free(queryPtr);
67+
68+
const resultStr = ptrToString(resultPtr);
69+
wasmModule._wasm_free_string(resultPtr);
70+
71+
if (resultStr === 'ERROR') {
72+
throw new Error('Operation failed');
73+
}
74+
75+
return JSON.parse(resultStr);
4676
});
4777

48-
const fingerprint = awaitInit((query) => {
49-
return new Promise(async (resolve, reject) => {
50-
PgQuery.fingerprintAsync(query, (err, result) => {
51-
err ? reject(err) : resolve(result);
52-
});
53-
});
78+
const fingerprint = awaitInit(async (query) => {
79+
const queryPtr = stringToPtr(query);
80+
const resultPtr = wasmModule._wasm_fingerprint(queryPtr);
81+
wasmModule._free(queryPtr);
82+
83+
const resultStr = ptrToString(resultPtr);
84+
wasmModule._wasm_free_string(resultPtr);
85+
86+
if (resultStr === 'ERROR') {
87+
throw new Error('Operation failed');
88+
}
89+
90+
return resultStr;
5491
});
5592

5693
module.exports = {

wasm/index.js

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,91 @@
1-
import { getDefaultContext } from '@emnapi/runtime';
21
import { pg_query } from '../proto.js';
32
import PgQueryModule from './libpg-query.js';
43

5-
let PgQuery;
4+
let wasmModule;
65

76
const initPromise = PgQueryModule().then((module) => {
8-
const binding = module.emnapiInit({
9-
context: getDefaultContext(),
10-
});
11-
12-
PgQuery = binding;
7+
wasmModule = module;
138
});
149

15-
/**
16-
* Function wrapper that waits for the WASM module to initialize
17-
* before executing the function.
18-
*/
1910
function awaitInit(fn) {
2011
return async (...args) => {
2112
await initPromise;
2213
return fn(...args);
2314
};
2415
}
2516

26-
export const parseQuery = awaitInit((query) => {
27-
return new Promise(async (resolve, reject) => {
28-
PgQuery.parseQueryAsync(query, (err, result) => {
29-
err ? reject(err) : resolve(JSON.parse(result));
30-
});
31-
});
17+
function stringToPtr(str) {
18+
const len = wasmModule.lengthBytesUTF8(str) + 1;
19+
const ptr = wasmModule._malloc(len);
20+
wasmModule.stringToUTF8(str, ptr, len);
21+
return ptr;
22+
}
23+
24+
function ptrToString(ptr) {
25+
return wasmModule.UTF8ToString(ptr);
26+
}
27+
28+
export const parseQuery = awaitInit(async (query) => {
29+
const queryPtr = stringToPtr(query);
30+
const resultPtr = wasmModule._wasm_parse_query(queryPtr);
31+
wasmModule._free(queryPtr);
32+
33+
const resultStr = ptrToString(resultPtr);
34+
wasmModule._wasm_free_string(resultPtr);
35+
36+
if (resultStr === 'ERROR') {
37+
throw new Error('Operation failed');
38+
}
39+
40+
return JSON.parse(resultStr);
3241
});
3342

34-
export const deparse = awaitInit((parseTree) => {
43+
export const deparse = awaitInit(async (parseTree) => {
3544
const msg = pg_query.ParseResult.fromObject(parseTree);
3645
const data = pg_query.ParseResult.encode(msg).finish();
37-
return new Promise((resolve, reject) => {
38-
PgQuery.deparseAsync(data, (err, result) => {
39-
err ? reject(err) : resolve(result);
40-
});
41-
});
46+
47+
const dataPtr = wasmModule._malloc(data.length);
48+
wasmModule.HEAPU8.set(data, dataPtr);
49+
50+
const resultPtr = wasmModule._wasm_deparse_protobuf(dataPtr, data.length);
51+
wasmModule._free(dataPtr);
52+
53+
const resultStr = ptrToString(resultPtr);
54+
wasmModule._wasm_free_string(resultPtr);
55+
56+
if (resultStr === 'ERROR') {
57+
throw new Error('Operation failed');
58+
}
59+
60+
return resultStr;
4261
});
4362

44-
export const parsePlPgSQL = awaitInit((query) => {
45-
return new Promise(async (resolve, reject) => {
46-
PgQuery.parsePlPgSQLAsync(query, (err, result) => {
47-
err ? reject(err) : resolve(JSON.parse(result));
48-
});
49-
});
63+
export const parsePlPgSQL = awaitInit(async (query) => {
64+
const queryPtr = stringToPtr(query);
65+
const resultPtr = wasmModule._wasm_parse_plpgsql(queryPtr);
66+
wasmModule._free(queryPtr);
67+
68+
const resultStr = ptrToString(resultPtr);
69+
wasmModule._wasm_free_string(resultPtr);
70+
71+
if (resultStr === 'ERROR') {
72+
throw new Error('Operation failed');
73+
}
74+
75+
return JSON.parse(resultStr);
5076
});
5177

52-
export const fingerprint = awaitInit((query) => {
53-
return new Promise(async (resolve, reject) => {
54-
PgQuery.fingerprintAsync(query, (err, result) => {
55-
err ? reject(err) : resolve(result);
56-
});
57-
});
78+
export const fingerprint = awaitInit(async (query) => {
79+
const queryPtr = stringToPtr(query);
80+
const resultPtr = wasmModule._wasm_fingerprint(queryPtr);
81+
wasmModule._free(queryPtr);
82+
83+
const resultStr = ptrToString(resultPtr);
84+
wasmModule._wasm_free_string(resultPtr);
85+
86+
if (resultStr === 'ERROR') {
87+
throw new Error('Operation failed');
88+
}
89+
90+
return resultStr;
5891
});

0 commit comments

Comments
 (0)