@cascode/cascode-js 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +98 -0
  2. package/binding.gyp +26 -0
  3. package/lib/std/Bundles.cas +6 -0
  4. package/lib/std/InverterLike.cas +11 -0
  5. package/lib/std/amp/Common.cas +16 -0
  6. package/lib/std/amp/FullyDifferentialOpAmp.cas +61 -0
  7. package/lib/std/amp/SingleEndedAmp.cas +48 -0
  8. package/lib/std/amp/SingleEndedOpAmp.cas +48 -0
  9. package/lib/std/bench/DCBiasBenches.cas +41 -0
  10. package/lib/std/bench/NoiseBenches.cas +167 -0
  11. package/lib/std/bench/PowerBenches.cas +202 -0
  12. package/lib/std/bench/TranBenches.cas +164 -0
  13. package/lib/std/bench/TransferBenches.cas +329 -0
  14. package/lib/std/composites/PadDriver.cas +10 -0
  15. package/lib/std/prim/Devices.cas +20 -0
  16. package/lib/std/prim/Passives.cas +23 -0
  17. package/lib/std/refs/ConstantGm.cas +25 -0
  18. package/lib/std/refs/CurrentReference.cas +7 -0
  19. package/lib/std/refs/ReferenceCircuit.cas +6 -0
  20. package/lib/std/refs/VoltageReference.cas +7 -0
  21. package/native/cascode_native_addon.c +788 -0
  22. package/native/osx-arm64/Cascode.Native.dylib +0 -0
  23. package/native/osx-arm64/google-ortools-native.dylib +0 -0
  24. package/native/osx-arm64/libortools.9.dylib +0 -0
  25. package/package.json +48 -0
  26. package/platform-packages/cascode-js-darwin-arm64/README.md +4 -0
  27. package/platform-packages/cascode-js-darwin-arm64/index.js +10 -0
  28. package/platform-packages/cascode-js-darwin-arm64/package.json +28 -0
  29. package/platform-packages/cascode-js-darwin-x64/README.md +4 -0
  30. package/platform-packages/cascode-js-darwin-x64/index.js +10 -0
  31. package/platform-packages/cascode-js-darwin-x64/package.json +28 -0
  32. package/platform-packages/cascode-js-linux-x64/README.md +4 -0
  33. package/platform-packages/cascode-js-linux-x64/index.js +10 -0
  34. package/platform-packages/cascode-js-linux-x64/package.json +28 -0
  35. package/platform-packages/cascode-js-win32-x64/README.md +4 -0
  36. package/platform-packages/cascode-js-win32-x64/index.js +10 -0
  37. package/platform-packages/cascode-js-win32-x64/package.json +28 -0
  38. package/scripts/stage-platform-package.mjs +128 -0
  39. package/scripts/sync-stdlib.mjs +35 -0
  40. package/src/index.d.ts +38 -0
  41. package/src/index.js +453 -0
@@ -0,0 +1,788 @@
1
+ #include <node_api.h>
2
+
3
+ #include <stdbool.h>
4
+ #include <stdint.h>
5
+ #include <stdio.h>
6
+ #include <stdlib.h>
7
+ #include <string.h>
8
+
9
+ #if defined(_WIN32)
10
+ #define WIN32_LEAN_AND_MEAN
11
+ #include <windows.h>
12
+ #include <io.h>
13
+ #define ACCESS _access
14
+ #define READ_OK 4
15
+ typedef HMODULE dylib_handle_t;
16
+ static SRWLOCK g_load_mutex = SRWLOCK_INIT;
17
+ /**
18
+ * Acquire the exclusive mutex used to serialize loading and unloading of the native library.
19
+ *
20
+ * Blocks until the lock is obtained. Protects the global load state (g_exports, g_attempted_load, g_load_error).
21
+ */
22
+ static void lock_load_mutex(void) { AcquireSRWLockExclusive(&g_load_mutex); }
23
+ /**
24
+ * Release exclusive ownership of the global load mutex used for library loading.
25
+ *
26
+ * Allows other threads to acquire the mutex and proceed with loading or accessing the cached exports.
27
+ */
28
+ static void unlock_load_mutex(void) { ReleaseSRWLockExclusive(&g_load_mutex); }
29
+ #elif defined(__linux__) || defined(__APPLE__)
30
+ #include <dlfcn.h>
31
+ #include <pthread.h>
32
+ #include <unistd.h>
33
+ #define ACCESS access
34
+ #define READ_OK R_OK
35
+ typedef void* dylib_handle_t;
36
+ static pthread_mutex_t g_load_mutex = PTHREAD_MUTEX_INITIALIZER;
37
+ /**
38
+ * Acquire the global load mutex to serialize dynamic library load/unload operations.
39
+ *
40
+ * Blocks the calling thread until the mutex protecting export loading is obtained.
41
+ */
42
+ static void lock_load_mutex(void) { pthread_mutex_lock(&g_load_mutex); }
43
+ /**
44
+ * Release the global mutex that protects one-time library loading.
45
+ *
46
+ * This function unlocks the mutex guarding the load/unload sequence for the
47
+ * native cascode library so other threads may proceed with load-related work.
48
+ */
49
+ static void unlock_load_mutex(void) { pthread_mutex_unlock(&g_load_mutex); }
50
+ #else
51
+ #error "cascode_native_addon currently supports linux, macOS, and Windows."
52
+ #endif
53
+
54
+ typedef int32_t (*cascode_create_session_fn)(const char* options_json_utf8);
55
+ typedef void (*cascode_destroy_session_fn)(int32_t session);
56
+ typedef void (*cascode_free_string_fn)(char* ptr);
57
+ typedef char* (*cascode_last_error_json_fn)(int32_t session);
58
+ typedef char* (*cascode_session_call_fn)(int32_t session, const char* request_json);
59
+ typedef char* (*cascode_version_fn)(void);
60
+
61
+ typedef struct cascode_exports_s {
62
+ dylib_handle_t handle;
63
+ cascode_create_session_fn create_session;
64
+ cascode_destroy_session_fn destroy_session;
65
+ cascode_free_string_fn free_string;
66
+ cascode_last_error_json_fn last_error_json;
67
+ cascode_session_call_fn document_open;
68
+ cascode_session_call_fn document_update_text;
69
+ cascode_session_call_fn document_close;
70
+ cascode_session_call_fn convert_to_structural;
71
+ cascode_session_call_fn convert_to_cas;
72
+ cascode_session_call_fn render_schematic;
73
+ cascode_session_call_fn schematic_apply_ops;
74
+ cascode_session_call_fn erc_run;
75
+ cascode_session_call_fn emit_run;
76
+ cascode_session_call_fn verify_run;
77
+ cascode_session_call_fn command_execute;
78
+ cascode_session_call_fn job_start;
79
+ cascode_session_call_fn job_poll;
80
+ cascode_session_call_fn job_cancel;
81
+ cascode_session_call_fn pdk_set_dir;
82
+ cascode_session_call_fn pdk_scan;
83
+ cascode_session_call_fn pdk_emit_primitives;
84
+ cascode_version_fn api_version;
85
+ cascode_version_fn schema_version;
86
+ } cascode_exports_t;
87
+
88
+ typedef struct method_entry_s {
89
+ const char* method;
90
+ cascode_session_call_fn fn;
91
+ } method_entry_t;
92
+
93
+ static cascode_exports_t g_exports;
94
+ static bool g_attempted_load = false;
95
+ static char g_load_error[512];
96
+
97
+ /**
98
+ * Set or clear the module load error message stored in the internal buffer.
99
+ *
100
+ * If `message` is NULL the stored load error is cleared; otherwise the provided
101
+ * message is copied into the internal `g_load_error` buffer (truncated to fit).
102
+ *
103
+ * @param message Error message to store, or NULL to clear the stored error.
104
+ */
105
+ static void set_load_error(const char* message) {
106
+ if (message == NULL) {
107
+ g_load_error[0] = '\0';
108
+ return;
109
+ }
110
+
111
+ snprintf(g_load_error, sizeof(g_load_error), "%s", message);
112
+ }
113
+
114
+ /**
115
+ * Extract the directory component from a filesystem path.
116
+ *
117
+ * Copies the directory part of `path` into `out`. If `path` contains no
118
+ * directory separator ('/' or '\'), writes "." into `out`. The result is
119
+ * always NUL-terminated; if the directory component is longer than
120
+ * `out_size - 1`, it is truncated to fit.
121
+ *
122
+ * @param path Input filesystem path (may use '/' or '\' separators).
123
+ * @param out Destination buffer to receive the directory component.
124
+ * @param out_size Size of `out` in bytes; must be greater than 0.
125
+ */
126
+ static void extract_directory(const char* path, char* out, size_t out_size) {
127
+ const char* slash_forward = strrchr(path, '/');
128
+ const char* slash_backward = strrchr(path, '\\');
129
+ const char* slash = slash_forward;
130
+ if (slash == NULL || (slash_backward != NULL && slash_backward > slash)) {
131
+ slash = slash_backward;
132
+ }
133
+ if (slash == NULL) {
134
+ snprintf(out, out_size, ".");
135
+ return;
136
+ }
137
+
138
+ size_t length = (size_t)(slash - path);
139
+ if (length >= out_size) {
140
+ length = out_size - 1;
141
+ }
142
+
143
+ memcpy(out, path, length);
144
+ out[length] = '\0';
145
+ }
146
+
147
+ /**
148
+ * Attempt to preload a native dependency file from the given directory.
149
+ *
150
+ * Constructs a full path from `directory` and `file_name` and, if the file is accessible,
151
+ * tries to load it into the process (platform-specific). On load failure records a
152
+ * human-readable message via set_load_error().
153
+ *
154
+ * @param directory Directory containing the dependency; may be a relative or absolute path.
155
+ * @param file_name File name of the dependency to preload.
156
+ * @returns `true` if the dependency is either not present/readable or was successfully loaded;
157
+ * `false` if the file was present but failed to load (and a load error was recorded).
158
+ */
159
+ static bool preload_dependency(const char* directory, const char* file_name) {
160
+ char full_path[1024];
161
+ snprintf(full_path, sizeof(full_path), "%s/%s", directory, file_name);
162
+ if (ACCESS(full_path, READ_OK) != 0) {
163
+ return true;
164
+ }
165
+
166
+ #if defined(_WIN32)
167
+ HMODULE handle = LoadLibraryA(full_path);
168
+ if (handle == NULL) {
169
+ DWORD code = GetLastError();
170
+ char windows_error[256];
171
+ snprintf(windows_error, sizeof(windows_error), "Windows error code %lu", (unsigned long)code);
172
+
173
+ char buffer[512];
174
+ snprintf(buffer, sizeof(buffer), "Failed to preload '%s': %s", full_path, windows_error);
175
+ set_load_error(buffer);
176
+ return false;
177
+ }
178
+ #else
179
+ void* handle = dlopen(full_path, RTLD_NOW | RTLD_GLOBAL);
180
+ if (handle == NULL) {
181
+ const char* dl_error = dlerror();
182
+ if (dl_error == NULL) {
183
+ dl_error = "unknown dependency load error";
184
+ }
185
+
186
+ char buffer[512];
187
+ snprintf(buffer, sizeof(buffer), "Failed to preload '%s': %s", full_path, dl_error);
188
+ set_load_error(buffer);
189
+ return false;
190
+ }
191
+ #endif
192
+
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Resolve a symbol by name from the currently loaded native library and store its address.
198
+ *
199
+ * On success writes the resolved pointer into `*target`. On failure records a load error
200
+ * message via set_load_error and leaves `*target` set to NULL.
201
+ *
202
+ * @param target Pointer to receive the resolved symbol address (stores NULL on failure).
203
+ * @param name NUL-terminated symbol name to resolve.
204
+ * @returns `true` if the symbol was resolved and stored in `*target`, `false` otherwise.
205
+ */
206
+ static bool resolve_symbol(void** target, const char* name) {
207
+ #if defined(_WIN32)
208
+ *target = (void*)GetProcAddress(g_exports.handle, name);
209
+ #else
210
+ *target = dlsym(g_exports.handle, name);
211
+ #endif
212
+ if (*target == NULL) {
213
+ char buffer[512];
214
+ snprintf(buffer, sizeof(buffer), "Failed to resolve symbol '%s'.", name);
215
+ set_load_error(buffer);
216
+ return false;
217
+ }
218
+
219
+ return true;
220
+ }
221
+
222
+ /**
223
+ * Unload the currently loaded cascode native library and clear the cached exports.
224
+ *
225
+ * If no library is loaded, this function does nothing. After it returns the global
226
+ * g_exports structure is zeroed, removing the stored library handle and function pointers.
227
+ */
228
+ static void unload_exports(void) {
229
+ if (g_exports.handle != NULL) {
230
+ #if defined(_WIN32)
231
+ FreeLibrary(g_exports.handle);
232
+ #else
233
+ dlclose(g_exports.handle);
234
+ #endif
235
+ }
236
+
237
+ memset(&g_exports, 0, sizeof(g_exports));
238
+ }
239
+
240
+ /**
241
+ * Attempt to load the Cascode native library and resolve all required symbols.
242
+ *
243
+ * Loads the library specified by the CASCODE_NATIVE_LIB environment variable,
244
+ * preloads platform-specific dependencies, resolves required exported symbols
245
+ * into g_exports, and records any load or symbol resolution error in g_load_error.
246
+ * This function is mutex-protected and performs the load operation at most once;
247
+ * subsequent calls return the cached result.
248
+ *
249
+ * @returns `true` if the library was loaded and all required symbols were resolved,
250
+ * `false` otherwise.
251
+ */
252
+ static bool load_exports(void) {
253
+ bool success = false;
254
+ lock_load_mutex();
255
+
256
+ if (g_attempted_load) {
257
+ success = g_exports.handle != NULL;
258
+ unlock_load_mutex();
259
+ return success;
260
+ }
261
+
262
+ g_attempted_load = true;
263
+ set_load_error(NULL);
264
+
265
+ const char* library_path = getenv("CASCODE_NATIVE_LIB");
266
+ if (library_path == NULL || library_path[0] == '\0') {
267
+ set_load_error("CASCODE_NATIVE_LIB is not set.");
268
+ goto done;
269
+ }
270
+
271
+ char library_dir[1024];
272
+ extract_directory(library_path, library_dir, sizeof(library_dir));
273
+ #if defined(_WIN32)
274
+ SetDllDirectoryA(library_dir);
275
+ if (!preload_dependency(library_dir, "e_sqlite3.dll")) {
276
+ goto done;
277
+ }
278
+ if (!preload_dependency(library_dir, "libe_sqlite3.dll")) {
279
+ goto done;
280
+ }
281
+ if (!preload_dependency(library_dir, "google-ortools-native.dll")) {
282
+ goto done;
283
+ }
284
+ #elif defined(__APPLE__)
285
+ if (!preload_dependency(library_dir, "libe_sqlite3.dylib")) {
286
+ goto done;
287
+ }
288
+
289
+ if (!preload_dependency(library_dir, "libortools.9.dylib")) {
290
+ goto done;
291
+ }
292
+
293
+ if (!preload_dependency(library_dir, "google-ortools-native.dylib")) {
294
+ goto done;
295
+ }
296
+ #else
297
+ if (!preload_dependency(library_dir, "libe_sqlite3.so")) {
298
+ goto done;
299
+ }
300
+ if (!preload_dependency(library_dir, "e_sqlite3.so")) {
301
+ goto done;
302
+ }
303
+ if (!preload_dependency(library_dir, "libortools.so.9")) {
304
+ goto done;
305
+ }
306
+
307
+ if (!preload_dependency(library_dir, "google-ortools-native.so")) {
308
+ goto done;
309
+ }
310
+ #endif
311
+
312
+ #if defined(_WIN32)
313
+ g_exports.handle = LoadLibraryA(library_path);
314
+ #else
315
+ g_exports.handle = dlopen(library_path, RTLD_NOW | RTLD_LOCAL);
316
+ #endif
317
+ if (g_exports.handle == NULL) {
318
+ #if defined(_WIN32)
319
+ DWORD code = GetLastError();
320
+ char windows_error[256];
321
+ snprintf(windows_error, sizeof(windows_error), "Windows error code %lu", (unsigned long)code);
322
+ #else
323
+ const char* dl_error = dlerror();
324
+ if (dl_error == NULL) {
325
+ dl_error = "Unknown dlopen error.";
326
+ }
327
+ #endif
328
+
329
+ char buffer[512];
330
+ #if defined(_WIN32)
331
+ snprintf(buffer, sizeof(buffer), "Failed to load '%s': %s", library_path, windows_error);
332
+ #else
333
+ snprintf(buffer, sizeof(buffer), "Failed to load '%s': %s", library_path, dl_error);
334
+ #endif
335
+ set_load_error(buffer);
336
+ goto done;
337
+ }
338
+
339
+ if (!resolve_symbol((void**)&g_exports.create_session, "cascode_create_session")) goto done;
340
+ if (!resolve_symbol((void**)&g_exports.destroy_session, "cascode_destroy_session")) goto done;
341
+ if (!resolve_symbol((void**)&g_exports.free_string, "cascode_free_string")) goto done;
342
+ if (!resolve_symbol((void**)&g_exports.last_error_json, "cascode_last_error_json")) goto done;
343
+ if (!resolve_symbol((void**)&g_exports.api_version, "cascode_api_version")) goto done;
344
+ if (!resolve_symbol((void**)&g_exports.schema_version, "cascode_schema_version")) goto done;
345
+ if (!resolve_symbol((void**)&g_exports.document_open, "cascode_document_open")) goto done;
346
+ if (!resolve_symbol((void**)&g_exports.document_update_text, "cascode_document_update_text")) goto done;
347
+ if (!resolve_symbol((void**)&g_exports.document_close, "cascode_document_close")) goto done;
348
+ if (!resolve_symbol((void**)&g_exports.convert_to_structural, "cascode_convert_to_structural")) goto done;
349
+ if (!resolve_symbol((void**)&g_exports.convert_to_cas, "cascode_convert_to_cas")) goto done;
350
+ if (!resolve_symbol((void**)&g_exports.render_schematic, "cascode_render_schematic")) goto done;
351
+ if (!resolve_symbol((void**)&g_exports.schematic_apply_ops, "cascode_schematic_apply_ops")) goto done;
352
+ if (!resolve_symbol((void**)&g_exports.erc_run, "cascode_erc_run")) goto done;
353
+ if (!resolve_symbol((void**)&g_exports.emit_run, "cascode_emit_run")) goto done;
354
+ if (!resolve_symbol((void**)&g_exports.verify_run, "cascode_verify_run")) goto done;
355
+ if (!resolve_symbol((void**)&g_exports.command_execute, "cascode_command_execute")) goto done;
356
+ if (!resolve_symbol((void**)&g_exports.job_start, "cascode_job_start")) goto done;
357
+ if (!resolve_symbol((void**)&g_exports.job_poll, "cascode_job_poll")) goto done;
358
+ if (!resolve_symbol((void**)&g_exports.job_cancel, "cascode_job_cancel")) goto done;
359
+ if (!resolve_symbol((void**)&g_exports.pdk_set_dir, "cascode_pdk_set_dir")) goto done;
360
+ if (!resolve_symbol((void**)&g_exports.pdk_scan, "cascode_pdk_scan")) goto done;
361
+ if (!resolve_symbol((void**)&g_exports.pdk_emit_primitives, "cascode_pdk_emit_primitives")) goto done;
362
+ success = true;
363
+
364
+ done:
365
+ if (!success) {
366
+ unload_exports();
367
+ }
368
+ unlock_load_mutex();
369
+ return success;
370
+ }
371
+
372
+ /**
373
+ * Ensure the Cascode native library exports are loaded and available to call.
374
+ *
375
+ * If loading fails, throws a N-API error `CASCODE_NATIVE_LOAD_FAILED` with a recorded diagnostic message.
376
+ *
377
+ * @returns `true` if the exports are loaded, `false` otherwise.
378
+ */
379
+ static bool ensure_loaded(napi_env env) {
380
+ if (load_exports()) {
381
+ return true;
382
+ }
383
+
384
+ const char* message = g_load_error[0] == '\0' ? "Failed to load libcascode exports." : g_load_error;
385
+ napi_throw_error(env, "CASCODE_NATIVE_LOAD_FAILED", message);
386
+ return false;
387
+ }
388
+
389
+ /**
390
+ * Read a JavaScript string and return a newly allocated UTF-8 NUL-terminated C string.
391
+ *
392
+ * The function converts the provided napi_value string into a heap-allocated
393
+ * UTF-8 C string and stores the pointer in *out_text. On success the caller
394
+ * owns the returned buffer and must free() it. On failure the function throws
395
+ * a N-API error and returns false.
396
+ *
397
+ * @param value JavaScript string value to read.
398
+ * @param out_text Receives a pointer to the newly allocated NUL-terminated UTF-8 string.
399
+ * Caller must free() this buffer when no longer needed.
400
+ * @returns `true` if the string was read and allocated successfully, `false` otherwise.
401
+ */
402
+ static bool read_utf8_arg(napi_env env, napi_value value, char** out_text) {
403
+ size_t length = 0;
404
+ napi_status status = napi_get_value_string_utf8(env, value, NULL, 0, &length);
405
+ if (status != napi_ok) {
406
+ napi_throw_type_error(env, "CASCODE_INVALID_ARGUMENT", "Expected a string.");
407
+ return false;
408
+ }
409
+
410
+ char* text = (char*)malloc(length + 1);
411
+ if (text == NULL) {
412
+ napi_throw_error(env, "CASCODE_NATIVE_OOM", "Failed to allocate UTF-8 buffer.");
413
+ return false;
414
+ }
415
+
416
+ size_t copied = 0;
417
+ status = napi_get_value_string_utf8(env, value, text, length + 1, &copied);
418
+ if (status != napi_ok) {
419
+ napi_throw_type_error(env, "CASCODE_INVALID_ARGUMENT", "Expected a string.");
420
+ free(text);
421
+ return false;
422
+ }
423
+
424
+ text[copied] = '\0';
425
+ *out_text = text;
426
+ return true;
427
+ }
428
+
429
+ /**
430
+ * Convert a JavaScript value to a 32-bit integer and store it in the provided output pointer.
431
+ * @param env The N-API environment.
432
+ * @param value The JavaScript value to convert.
433
+ * @param out_value Pointer to an int32_t that receives the converted value on success.
434
+ * @returns `true` if the value was successfully converted to a 32-bit integer, `false` otherwise.
435
+ */
436
+ static bool read_int32_arg(napi_env env, napi_value value, int32_t* out_value) {
437
+ napi_status status = napi_get_value_int32(env, value, out_value);
438
+ return status == napi_ok;
439
+ }
440
+
441
+ /**
442
+ * Create a JavaScript string from a null-terminated UTF-8 C string.
443
+ * @param text Null-terminated UTF-8 text to convert to a JS string.
444
+ * @returns A `napi_value` representing the newly created JavaScript string.
445
+ */
446
+ static napi_value make_string(napi_env env, const char* text) {
447
+ napi_value result;
448
+ napi_create_string_utf8(env, text, NAPI_AUTO_LENGTH, &result);
449
+ return result;
450
+ }
451
+
452
+ /**
453
+ * Create a new Cascode session and return its session identifier to JavaScript.
454
+ *
455
+ * Reads an optional JSON options string from the first argument (defaults to "{}"),
456
+ * calls the native cascode create_session function, and returns the resulting session id.
457
+ *
458
+ * @param env N-API environment.
459
+ * @param info Callback info containing arguments and this-value.
460
+ * @returns A JavaScript Number containing the non-zero session id.
461
+ * @throws Throws a N-API error `CASCODE_NATIVE_LOAD_FAILED` if the native library cannot be loaded.
462
+ * @throws Throws a N-API error `CASCODE_CREATE_SESSION_FAILED` if the native create_session returns 0.
463
+ */
464
+ static napi_value js_create_session(napi_env env, napi_callback_info info) {
465
+ if (!ensure_loaded(env)) {
466
+ return NULL;
467
+ }
468
+
469
+ size_t argc = 1;
470
+ napi_value args[1];
471
+ napi_get_cb_info(env, info, &argc, args, NULL, NULL);
472
+
473
+ const char* default_options = "{}";
474
+ char* options_text = NULL;
475
+ const char* options = default_options;
476
+ if (argc >= 1) {
477
+ napi_valuetype value_type;
478
+ napi_typeof(env, args[0], &value_type);
479
+ if (value_type == napi_string) {
480
+ if (!read_utf8_arg(env, args[0], &options_text)) {
481
+ return NULL;
482
+ }
483
+
484
+ options = options_text;
485
+ }
486
+ }
487
+
488
+ int32_t session = g_exports.create_session(options);
489
+ if (options_text != NULL) {
490
+ free(options_text);
491
+ }
492
+
493
+ if (session == 0) {
494
+ napi_throw_error(env, "CASCODE_CREATE_SESSION_FAILED", "cascode_create_session returned 0.");
495
+ return NULL;
496
+ }
497
+
498
+ napi_value result;
499
+ napi_create_int32(env, session, &result);
500
+ return result;
501
+ }
502
+
503
+ /**
504
+ * Destroy a cascode session identified by its integer session handle.
505
+ *
506
+ * Validates a single numeric argument (session). On success calls the native
507
+ * destroy_session implementation and returns JavaScript `undefined`.
508
+ *
509
+ * @param env N-API environment.
510
+ * @param info N-API callback info containing the arguments.
511
+ * @returns A napi_value representing JavaScript `undefined`.
512
+ * @throws TypeError if the argument count is not 1 or if the session is not an integer.
513
+ */
514
+ static napi_value js_destroy_session(napi_env env, napi_callback_info info) {
515
+ if (!ensure_loaded(env)) {
516
+ return NULL;
517
+ }
518
+
519
+ size_t argc = 1;
520
+ napi_value args[1];
521
+ napi_get_cb_info(env, info, &argc, args, NULL, NULL);
522
+ if (argc < 1) {
523
+ napi_throw_type_error(env, "CASCODE_INVALID_ARGUMENT", "destroySession(session) requires 1 argument.");
524
+ return NULL;
525
+ }
526
+
527
+ int32_t session = 0;
528
+ if (!read_int32_arg(env, args[0], &session)) {
529
+ napi_throw_type_error(env, "CASCODE_INVALID_ARGUMENT", "session must be an integer.");
530
+ return NULL;
531
+ }
532
+
533
+ g_exports.destroy_session(session);
534
+ napi_value result;
535
+ napi_get_undefined(env, &result);
536
+ return result;
537
+ }
538
+
539
+ /**
540
+ * Retrieve the last error JSON for a session.
541
+ *
542
+ * @param session Session identifier returned by create_session.
543
+ * @returns Pointer to a NUL-terminated JSON string describing the last error for the session, or NULL if no error information is available.
544
+ */
545
+ static char* call_last_error_json(int32_t session) {
546
+ if (g_exports.last_error_json == NULL) {
547
+ return NULL;
548
+ }
549
+
550
+ return g_exports.last_error_json(session);
551
+ }
552
+
553
+ /**
554
+ * Throw a JavaScript CASCODE_CALL_FAILED exception using the session's last error JSON if available; otherwise throw a generic CASCODE_CALL_FAILED error.
555
+ * @param env N-API environment.
556
+ * @param session Cascode session identifier used to query the last error JSON.
557
+ * @returns NULL; the function always throws a JavaScript exception and does not return a normal value.
558
+ */
559
+ static napi_value throw_call_error(napi_env env, int32_t session) {
560
+ char* error_json = call_last_error_json(session);
561
+ if (error_json != NULL) {
562
+ napi_throw_error(env, "CASCODE_CALL_FAILED", error_json);
563
+ g_exports.free_string(error_json);
564
+ return NULL;
565
+ }
566
+
567
+ napi_throw_error(env, "CASCODE_CALL_FAILED", "Cascode call returned null.");
568
+ return NULL;
569
+ }
570
+
571
+ /**
572
+ * Map a cascode method name to its corresponding session-call function.
573
+ *
574
+ * Looks up the exact, case-sensitive method name and returns the associated
575
+ * function pointer that implements that session call.
576
+ *
577
+ * @param method_name Null-terminated UTF-8 method name to resolve.
578
+ * @returns The `cascode_session_call_fn` for the given method name, or `NULL`
579
+ * if the method name is not recognized.
580
+ */
581
+ static cascode_session_call_fn resolve_method_fn(const char* method_name) {
582
+ const method_entry_t table[] = {
583
+ {"document.open", g_exports.document_open},
584
+ {"document.updateText", g_exports.document_update_text},
585
+ {"document.close", g_exports.document_close},
586
+ {"convert.toStructural", g_exports.convert_to_structural},
587
+ {"convert.toCas", g_exports.convert_to_cas},
588
+ {"render.schematic", g_exports.render_schematic},
589
+ {"schematic.applyOperations", g_exports.schematic_apply_ops},
590
+ {"erc.run", g_exports.erc_run},
591
+ {"emit.run", g_exports.emit_run},
592
+ {"verify.run", g_exports.verify_run},
593
+ {"command.execute", g_exports.command_execute},
594
+ {"job.start", g_exports.job_start},
595
+ {"job.poll", g_exports.job_poll},
596
+ {"job.cancel", g_exports.job_cancel},
597
+ {"pdk.setDir", g_exports.pdk_set_dir},
598
+ {"pdk.scan", g_exports.pdk_scan},
599
+ {"pdk.emitPrimitives", g_exports.pdk_emit_primitives},
600
+ };
601
+
602
+ size_t count = sizeof(table) / sizeof(table[0]);
603
+ for (size_t i = 0; i < count; i++) {
604
+ if (strcmp(table[i].method, method_name) == 0) {
605
+ return table[i].fn;
606
+ }
607
+ }
608
+
609
+ return NULL;
610
+ }
611
+
612
+ /**
613
+ * Invoke a named Cascode session method with a JSON request and return its JSON response as a JavaScript string.
614
+ *
615
+ * Expects three JavaScript arguments: `session` (int32), `method` (string), and `requestJson` (string).
616
+ * Throws a JS TypeError for missing/invalid arguments or unknown method names. If the native method call
617
+ * returns NULL, throws an error containing the session's last error JSON (when available) or a generic message.
618
+ *
619
+ * @returns A `napi_value` containing the response JSON as a JavaScript string, or `NULL` if an exception was thrown.
620
+ */
621
+ static napi_value js_call(napi_env env, napi_callback_info info) {
622
+ if (!ensure_loaded(env)) {
623
+ return NULL;
624
+ }
625
+
626
+ size_t argc = 3;
627
+ napi_value args[3];
628
+ napi_get_cb_info(env, info, &argc, args, NULL, NULL);
629
+ if (argc < 3) {
630
+ napi_throw_type_error(env, "CASCODE_INVALID_ARGUMENT", "call(session, method, requestJson) requires 3 arguments.");
631
+ return NULL;
632
+ }
633
+
634
+ int32_t session = 0;
635
+ if (!read_int32_arg(env, args[0], &session)) {
636
+ napi_throw_type_error(env, "CASCODE_INVALID_ARGUMENT", "session must be an integer.");
637
+ return NULL;
638
+ }
639
+
640
+ char* method_name = NULL;
641
+ char* request_json = NULL;
642
+ if (!read_utf8_arg(env, args[1], &method_name)) {
643
+ return NULL;
644
+ }
645
+
646
+ if (!read_utf8_arg(env, args[2], &request_json)) {
647
+ free(method_name);
648
+ return NULL;
649
+ }
650
+
651
+ cascode_session_call_fn method_fn = resolve_method_fn(method_name);
652
+ if (method_fn == NULL) {
653
+ free(method_name);
654
+ free(request_json);
655
+ napi_throw_type_error(env, "CASCODE_INVALID_METHOD", "Unknown method name.");
656
+ return NULL;
657
+ }
658
+
659
+ char* response_json = method_fn(session, request_json);
660
+ free(method_name);
661
+ free(request_json);
662
+
663
+ if (response_json == NULL) {
664
+ return throw_call_error(env, session);
665
+ }
666
+
667
+ napi_value result = make_string(env, response_json);
668
+ g_exports.free_string(response_json);
669
+ return result;
670
+ }
671
+
672
+ /**
673
+ * Return the last error JSON for a Cascode session as a JavaScript string or `null` if none exists.
674
+ *
675
+ * Expects one integer argument: the session id. If the native library is not loaded this function
676
+ * will propagate the load error.
677
+ *
678
+ * @returns napi_value string containing the last error JSON for the session, or `null` if there is no error.
679
+ * @throws JS TypeError with code "CASCODE_INVALID_ARGUMENT" if the session argument is missing or not an integer.
680
+ * @throws JS Error "CASCODE_NATIVE_LOAD_FAILED" if the native Cascode library cannot be loaded.
681
+ */
682
+ static napi_value js_last_error_json(napi_env env, napi_callback_info info) {
683
+ if (!ensure_loaded(env)) {
684
+ return NULL;
685
+ }
686
+
687
+ size_t argc = 1;
688
+ napi_value args[1];
689
+ napi_get_cb_info(env, info, &argc, args, NULL, NULL);
690
+ if (argc < 1) {
691
+ napi_throw_type_error(env, "CASCODE_INVALID_ARGUMENT", "lastErrorJson(session) requires 1 argument.");
692
+ return NULL;
693
+ }
694
+
695
+ int32_t session = 0;
696
+ if (!read_int32_arg(env, args[0], &session)) {
697
+ napi_throw_type_error(env, "CASCODE_INVALID_ARGUMENT", "session must be an integer.");
698
+ return NULL;
699
+ }
700
+
701
+ char* error_json = call_last_error_json(session);
702
+ if (error_json == NULL) {
703
+ napi_value null_value;
704
+ napi_get_null(env, &null_value);
705
+ return null_value;
706
+ }
707
+
708
+ napi_value result = make_string(env, error_json);
709
+ g_exports.free_string(error_json);
710
+ return result;
711
+ }
712
+
713
+ /**
714
+ * Retrieve the cascode library API version string.
715
+ *
716
+ * @param env The N-API environment.
717
+ * @returns A JavaScript string containing the API version, or NULL if an exception was thrown.
718
+ * @throws CASCODE_VERSION_FAILED if the cascode api_version call returns NULL.
719
+ */
720
+ static napi_value js_api_version(napi_env env, napi_callback_info info) {
721
+ (void)info;
722
+ if (!ensure_loaded(env)) {
723
+ return NULL;
724
+ }
725
+
726
+ char* value = g_exports.api_version();
727
+ if (value == NULL) {
728
+ napi_throw_error(env, "CASCODE_VERSION_FAILED", "cascode_api_version returned null.");
729
+ return NULL;
730
+ }
731
+
732
+ napi_value result = make_string(env, value);
733
+ g_exports.free_string(value);
734
+ return result;
735
+ }
736
+
737
+ /**
738
+ * Return the Cascode schema version as a JavaScript string.
739
+ *
740
+ * Calls the loaded Cascode library's schema_version function and converts
741
+ * the returned UTF-8 C string into a napi_value JavaScript string.
742
+ *
743
+ * @returns napi_value A JavaScript string containing the schema version, or NULL if an error was thrown.
744
+ * @throws CASCODE_NATIVE_LOAD_FAILED if the native library could not be loaded.
745
+ * @throws CASCODE_VERSION_FAILED if the Cascode `schema_version` call returned NULL.
746
+ */
747
+ static napi_value js_schema_version(napi_env env, napi_callback_info info) {
748
+ (void)info;
749
+ if (!ensure_loaded(env)) {
750
+ return NULL;
751
+ }
752
+
753
+ char* value = g_exports.schema_version();
754
+ if (value == NULL) {
755
+ napi_throw_error(env, "CASCODE_VERSION_FAILED", "cascode_schema_version returned null.");
756
+ return NULL;
757
+ }
758
+
759
+ napi_value result = make_string(env, value);
760
+ g_exports.free_string(value);
761
+ return result;
762
+ }
763
+
764
+ /**
765
+ * Attach the native addon methods to the provided module exports object.
766
+ *
767
+ * Defines and binds the following functions on `exports`: createSession,
768
+ * destroySession, call, lastErrorJson, apiVersion, and schemaVersion.
769
+ *
770
+ * @param env The N-API environment.
771
+ * @param exports The target exports object to receive the addon properties.
772
+ * @returns The same `exports` object with the native functions defined on it.
773
+ */
774
+ static napi_value init(napi_env env, napi_value exports) {
775
+ napi_property_descriptor descriptors[] = {
776
+ {"createSession", NULL, js_create_session, NULL, NULL, NULL, napi_default, NULL},
777
+ {"destroySession", NULL, js_destroy_session, NULL, NULL, NULL, napi_default, NULL},
778
+ {"call", NULL, js_call, NULL, NULL, NULL, napi_default, NULL},
779
+ {"lastErrorJson", NULL, js_last_error_json, NULL, NULL, NULL, napi_default, NULL},
780
+ {"apiVersion", NULL, js_api_version, NULL, NULL, NULL, napi_default, NULL},
781
+ {"schemaVersion", NULL, js_schema_version, NULL, NULL, NULL, napi_default, NULL},
782
+ };
783
+
784
+ napi_define_properties(env, exports, sizeof(descriptors) / sizeof(descriptors[0]), descriptors);
785
+ return exports;
786
+ }
787
+
788
+ NAPI_MODULE(NODE_GYP_MODULE_NAME, init)