jetc: add #api directive + jetc_api_<name> route metadata export

Stage 1 of the APIServer plan: a compiled --shared .so currently exports
only a bare jetc_render_<name>(Global&, MString&) with no way for a host
to discover what HTTP method+path it wants to serve. Adds a leading
"#api METHOD /path" directive (one or more, parsed the same way jetc
already strips a leading "#!" shebang line - not modeled as a new
AST/tag-attribute concept, since jetc's parser has no existing notion of
the enclosing <jet> tag's attributes to hang this off of).

In --shared mode, JetCodegen now also emits a second exported function
alongside the render entry point:

  struct JetApiRoute { const char *method; const char *path; };
  extern "C" const JetApiRoute *jetc_api_<name>(int *count) { ... }

<name> is derived the same way for both symbols (sanitizeIdent(
baseNameNoExt(inputPath))), so a future host loader can find both by
convention from just the .so's filename - no name needs to be embedded
in the metadata itself, and the routes a module declares can never drift
out of sync with the render function that implements them (the failure
mode a sidecar manifest file would have).

Malformed directives (missing method/path, path without a leading '/')
are a hard compile error; #api directives present without --shared print
a non-fatal warning (the metadata has nowhere to go without a host to
load a .so, but the render-only executable/CGI path still works fine).

Verified live, not just compiled: rebuilt jetc, compiled demo3.jet (a new
worked example with two #api lines sharing one handler) via
build-shared.sh into a real demo3.so, and added api_test.cpp (mirrors
host_test.cpp's dlopen()/dlsym() pattern) confirming jetc_api_demo3
returns the expected two routes at runtime. Re-ran host_test against the
same .so to confirm the render path is unaffected. Confirmed demo.jet/
demo2.jet (no #api lines) emit no JetApiRoute code at all - no regression
for templates that don't declare routes.

Also fixed a gitignore gap surfaced while committing this: an earlier
"jetc/*.cpp" ignore rule (meant for generated *.compiled.cpp output) was
a blanket pattern that silently hid this commit's own api_test.cpp from
git status. Narrowed to "jetc/*.compiled.cpp" (the actual generated-file
naming, per build-compiled.sh/build-shared.sh) and added explicit ignores
for the host_test/api_test/demo3 binaries plus two known stale generated
leftovers (demo.cpp/demo2.cpp) that predate this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Brad ArantandClaude Sonnet 5 committed 2026-08-25 10:24:10 -07:00
1 parent a32444acc2
commit 634eb7a9a9
6 files changed
+135 -15

No files matched your search

+16 -1
View File
@@ -173,7 +173,8 @@ namespace jetc {
}
std::string Codegen::generate(const std::vector<NodePtr> &top, const std::string &sourceFileComment,
bool sharedMode, const std::string &entrySymbol) {
bool sharedMode, const std::string &entrySymbol,
const std::vector<ApiRoute> &apiRoutes, const std::string &apiSymbol) {
out.str("");
out.clear();
out << "// " << sourceFileComment << "\n";
@@ -187,6 +188,20 @@ namespace jetc {
out << "#include <iostream>\n\n";
if (sharedMode) {
if (!apiRoutes.empty()) {
// A plain C struct, not any CoreUtils/JetCore type - this is the
// one piece of the plugin ABI a host loader needs to know about
// independent of the rest of this same-ABI C++ boundary.
out << "struct JetApiRoute { const char *method; const char *path; };\n";
out << "extern \"C\" const JetApiRoute *" << apiSymbol << "(int *count) {\n";
out << " static const JetApiRoute routes[] = {\n";
for (auto &r : apiRoutes)
out << " { " << cstr(r.method) << ", " << cstr(r.path) << " },\n";
out << " };\n";
out << " *count = " << apiRoutes.size() << ";\n";
out << " return routes;\n";
out << "}\n\n";
}
out << "extern \"C\" void " << entrySymbol << "(jet::Global &global, coreutils::MString &out) {\n";
out << " coreutils::ZString jetc_empty(\"\");\n";
out << " coreutils::MString jetc_scopeOut;\n";