SWIG Escape Hatch — Manual .i Files
cpp.js auto-generates SWIG .i interface files for every header it sees. You only write a manual .i file when the auto-generated one isn't enough. Default to letting cpp.js generate — fall back to manual only when forced.
How auto-generation works
For each header in paths.header (defaults to paths.native, defaults to src/native), cpp.js generates an .i file with:
%module FILENAME_UPPER
%feature("shared_ptr")
%feature("polymorphic_shared_ptr")
%include "path/to/header.h"
That's it. Header included verbatim, shared_ptr features enabled, module name derived from filename. For 95% of cases this is correct.
When you need to write your own
You're forced into manual .i only when one of these is true:
- Custom type mappings (
%typemapdirectives) — converting an exotic upstream type to a JS-friendly one. - Selective symbol export — your header has 100 symbols and you want to expose only 10. Auto-generated
.iexposes everything. - Renaming (
%rename) — your C++ symbol clashes with a JS reserved word, or you want a more idiomatic JS name. - Ignoring members (
%ignore) — a method takes a raw pointer that the auto-binder would choke on, and you can't refactor the upstream header. - Custom directors for cross-language polymorphism (rare).
For everything else, write a C++ wrapper instead. Wrappers are easier to reason about and won't drift if SWIG semantics change.
How to wire a manual .i into your project
cpp.js looks for an .i file using two mechanisms (in order):
Option 1 — sibling next to header
If src/native/foo.h has a sibling src/native/foo.i, cpp.js uses the .i instead of auto-generating. Same path + .i extension is the trigger.
src/native/
├── foo.h # the header
└── foo.i # YOUR custom interface — overrides auto-gen
Option 2 — paths.module override
If your .i files live elsewhere, point cppjs.config.js at them:
export default {
general: { name: 'myapp' },
paths: {
config: import.meta.url,
native: ['src/native'],
module: ['src/swig'], // .i files live here
},
}
cpp.js reads paths.module (defaults to paths.native) for .i files. The ext.module field controls extensions to recognize (defaults to ['i']).
Minimal .i template
%module mymodule
%{
#include "myheader.h"
%}
%feature("shared_ptr")
%feature("polymorphic_shared_ptr")
// Optional: rename a method to be JS-idiomatic
%rename(processData) MyClass::process_data;
// Optional: ignore an unbindable method
%ignore MyClass::raw_pointer_method;
// Include the header (so SWIG sees the declarations)
%include "myheader.h"
Replace mymodule with FILENAME_UPPER (the convention auto-gen uses). Drop into src/native/myheader.i next to myheader.h.
Caveats
- Manual
.ioverrides the auto-gen entirely. You're responsible for%feature("shared_ptr")and the rest. Forget them, andshared_ptrreturns become opaque pointers in JS. %include "myheader.h"is what tells SWIG about your symbols. Without it, the.iis empty even with the%{ #include ... %}block (that one only injects into the generated wrapper, not into SWIG's symbol table).- Build cache keys on header hash, not
.ihash. If you only edit the.iand not the header, you may need to clear.cppjs/cache.jsonortouchthe header to retrigger the binding regen.
When you're past .i
If a .i file isn't enough either, the only remaining option is to wrap in C++ — see the wrapper pattern in C++ binding rules. Anything reachable from a clean wrapper class with binding-friendly types will bind.
See also
- C++ binding rules — what auto-generated bindings can and can't handle.
- Paths —
paths.moduleandext.modulefields. - Troubleshooting — SWIG generation failures.
Source: createInterface.js — auto-generation logic.