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:
1 parent
a32444acc2
commit
634eb7a9a9
6 files changed
+135
-15
No files matched your search
+11
-4
@@ -6,13 +6,20 @@ docs/JetCore.dvi
|
|||||||
docs/JetCore.log
|
docs/JetCore.log
|
||||||
docs/JetCore.toc
|
docs/JetCore.toc
|
||||||
|
|
||||||
# jetc build byproducts (compiled JET-to-C++ output, binaries, .so/.a artifacts)
|
# jetc build byproducts (compiled JET-to-C++ output, binaries, .so/.a artifacts).
|
||||||
|
# Generated .cpp files are always named "*.compiled.cpp" (see build-compiled.sh/
|
||||||
|
# build-shared.sh) - deliberately not a blanket "*.cpp", which would also
|
||||||
|
# swallow real source files added to jetc/ later (bit us once already: an
|
||||||
|
# earlier blanket rule silently hid api_test.cpp from git status).
|
||||||
jetc/jetc
|
jetc/jetc
|
||||||
jetc/demo
|
jetc/demo
|
||||||
jetc/demo2
|
jetc/demo2
|
||||||
jetc/*.cpp
|
jetc/demo3
|
||||||
|
jetc/host_test
|
||||||
|
jetc/api_test
|
||||||
|
jetc/demo.cpp
|
||||||
|
jetc/demo2.cpp
|
||||||
|
jetc/*.compiled.cpp
|
||||||
jetc/*.pic.o
|
jetc/*.pic.o
|
||||||
jetc/*.so
|
jetc/*.so
|
||||||
jetc/libCoreUtils_pic.a
|
jetc/libCoreUtils_pic.a
|
||||||
!jetc/demo.jet
|
|
||||||
!jetc/demo2.jet
|
|
||||||
+16
-1
@@ -173,7 +173,8 @@ namespace jetc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string Codegen::generate(const std::vector<NodePtr> &top, const std::string &sourceFileComment,
|
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.str("");
|
||||||
out.clear();
|
out.clear();
|
||||||
out << "// " << sourceFileComment << "\n";
|
out << "// " << sourceFileComment << "\n";
|
||||||
@@ -187,6 +188,20 @@ namespace jetc {
|
|||||||
out << "#include <iostream>\n\n";
|
out << "#include <iostream>\n\n";
|
||||||
|
|
||||||
if (sharedMode) {
|
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 << "extern \"C\" void " << entrySymbol << "(jet::Global &global, coreutils::MString &out) {\n";
|
||||||
out << " coreutils::ZString jetc_empty(\"\");\n";
|
out << " coreutils::ZString jetc_empty(\"\");\n";
|
||||||
out << " coreutils::MString jetc_scopeOut;\n";
|
out << " coreutils::MString jetc_scopeOut;\n";
|
||||||
|
|||||||
+22
-1
@@ -4,9 +4,19 @@
|
|||||||
#include "JetAST.h"
|
#include "JetAST.h"
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace jetc {
|
namespace jetc {
|
||||||
|
|
||||||
|
// One `#api METHOD /path` directive from the top of the source template
|
||||||
|
// (see jetc.cpp) - a compile-time-known route this template wants to
|
||||||
|
// serve once loaded as a .so. Purely descriptive data handed to Codegen;
|
||||||
|
// jetc itself never interprets the method/path beyond storing them.
|
||||||
|
struct ApiRoute {
|
||||||
|
std::string method;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
||||||
// Emits a standalone .cpp translation unit that renders a compiled JET
|
// Emits a standalone .cpp translation unit that renders a compiled JET
|
||||||
// template. The static tag/text structure becomes real C++ control flow;
|
// template. The static tag/text structure becomes real C++ control flow;
|
||||||
// anything genuinely dynamic (variable values, expr/value comparisons,
|
// anything genuinely dynamic (variable values, expr/value comparisons,
|
||||||
@@ -19,6 +29,8 @@ namespace jetc {
|
|||||||
// sharedMode == false (default): emits a static jetc_render() plus a
|
// sharedMode == false (default): emits a static jetc_render() plus a
|
||||||
// main() that runs it once, catches coreutils::Exception, and writes
|
// main() that runs it once, catches coreutils::Exception, and writes
|
||||||
// the result to stdout - the existing CGI-style standalone program.
|
// the result to stdout - the existing CGI-style standalone program.
|
||||||
|
// apiRoutes/apiSymbol are ignored in this mode (there's no host to
|
||||||
|
// dlsym them).
|
||||||
//
|
//
|
||||||
// sharedMode == true: emits no main() at all. Instead the render
|
// sharedMode == true: emits no main() at all. Instead the render
|
||||||
// logic is exported as `extern "C" void <entrySymbol>(jet::Global
|
// logic is exported as `extern "C" void <entrySymbol>(jet::Global
|
||||||
@@ -36,8 +48,17 @@ namespace jetc {
|
|||||||
// template (see jetc.cpp) so multiple .so's can coexist even if a
|
// template (see jetc.cpp) so multiple .so's can coexist even if a
|
||||||
// host ever dlopen()s them RTLD_GLOBAL instead of keeping separate
|
// host ever dlopen()s them RTLD_GLOBAL instead of keeping separate
|
||||||
// handles.
|
// handles.
|
||||||
|
//
|
||||||
|
// If apiRoutes is non-empty, sharedMode also emits a second exported
|
||||||
|
// function `extern "C" const JetApiRoute *<apiSymbol>(int *count)`
|
||||||
|
// returning a static array describing every `#api` directive the
|
||||||
|
// source declared - a host's directory watcher/loader dlsym()'s this
|
||||||
|
// (by the same filename-derived naming convention as entrySymbol) to
|
||||||
|
// learn what to register, without any separate manifest file that
|
||||||
|
// could drift out of sync with the render function it describes.
|
||||||
std::string generate(const std::vector<NodePtr> &top, const std::string &sourceFileComment,
|
std::string generate(const std::vector<NodePtr> &top, const std::string &sourceFileComment,
|
||||||
bool sharedMode = false, const std::string &entrySymbol = "jetc_render");
|
bool sharedMode = false, const std::string &entrySymbol = "jetc_render",
|
||||||
|
const std::vector<ApiRoute> &apiRoutes = {}, const std::string &apiSymbol = "jetc_api");
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::ostringstream out;
|
std::ostringstream out;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Minimal dlsym() check for the jetc_api_<name> route-metadata export added
|
||||||
|
// alongside jetc_render_<name> in --shared mode (see JetCodegen.cpp). Proves
|
||||||
|
// a host can discover a compiled module's declared routes purely from the
|
||||||
|
// .so itself, with no separate manifest file - the actual point of putting
|
||||||
|
// routing metadata in an exported symbol instead of a sidecar file.
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <iostream>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
struct JetApiRoute { const char *method; const char *path; };
|
||||||
|
typedef const JetApiRoute *(*ApiFn)(int *count);
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 3) {
|
||||||
|
std::cerr << "usage: api_test <module.so> <api-symbol>" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
void *handle = dlopen(argv[1], RTLD_NOW);
|
||||||
|
if (!handle) {
|
||||||
|
std::cerr << "dlopen failed: " << dlerror() << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
dlerror();
|
||||||
|
ApiFn api = (ApiFn)dlsym(handle, argv[2]);
|
||||||
|
const char *err = dlerror();
|
||||||
|
if (err) {
|
||||||
|
std::cerr << "dlsym failed: " << err << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = -1;
|
||||||
|
const JetApiRoute *routes = api(&count);
|
||||||
|
std::cout << count << " route(s):" << std::endl;
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
std::cout << " " << routes[i].method << " " << routes[i].path << std::endl;
|
||||||
|
|
||||||
|
dlclose(handle);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!../jet-2.0
|
||||||
|
#api GET /hello
|
||||||
|
#api HEAD /hello
|
||||||
|
Hello, API world.
|
||||||
+43
-9
@@ -58,24 +58,58 @@ int main(int argc, char **argv) {
|
|||||||
src = (nl == std::string::npos) ? std::string() : src.substr(nl + 1);
|
src = (nl == std::string::npos) ? std::string() : src.substr(nl + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consume zero or more leading "#api METHOD /path" directive lines
|
||||||
|
// (right after the optional shebang), the same way the shebang itself
|
||||||
|
// is stripped - a plain directive line, not a new tag/attribute concept,
|
||||||
|
// since jetc's parser has no existing notion of the enclosing <jet> tag's
|
||||||
|
// attributes to hang this off of. Each becomes one route this template
|
||||||
|
// wants to serve once loaded as a .so (see JetCodegen's apiRoutes).
|
||||||
|
std::vector<jetc::ApiRoute> apiRoutes;
|
||||||
|
while (src.rfind("#api ", 0) == 0) {
|
||||||
|
size_t nl = src.find('\n');
|
||||||
|
std::string line = (nl == std::string::npos) ? src : src.substr(0, nl);
|
||||||
|
src = (nl == std::string::npos) ? std::string() : src.substr(nl + 1);
|
||||||
|
|
||||||
|
std::istringstream ls(line);
|
||||||
|
std::string tag, method, path;
|
||||||
|
ls >> tag >> method >> path;
|
||||||
|
if (method.empty() || path.empty() || path[0] != '/') {
|
||||||
|
std::cerr << "jetc: malformed #api directive (expected \"#api METHOD /path\"): "
|
||||||
|
<< line << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
for (auto &c : method) c = (char)std::toupper((unsigned char)c);
|
||||||
|
apiRoutes.push_back({method, path});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
jetc::Parser parser(src);
|
jetc::Parser parser(src);
|
||||||
auto nodes = parser.parseNodes();
|
auto nodes = parser.parseNodes();
|
||||||
jetc::Codegen codegen;
|
jetc::Codegen codegen;
|
||||||
// Symbol is derived from the input filename (not the output path) so
|
// Both symbols are derived from the input filename (not the output
|
||||||
// it stays stable and predictable for whatever builds and dlsym()'s
|
// path) so they stay stable and predictable for whatever builds and
|
||||||
// the resulting .so - unique per template, so multiple compiled
|
// dlsym()'s the resulting .so - unique per template, so multiple
|
||||||
// templates' .so's can coexist in one process even under
|
// compiled templates' .so's can coexist in one process even under
|
||||||
// dlopen(..., RTLD_GLOBAL) instead of requiring isolated handles.
|
// dlopen(..., RTLD_GLOBAL) instead of requiring isolated handles.
|
||||||
std::string symbol = "jetc_render_" + sanitizeIdent(baseNameNoExt(inputPath));
|
std::string baseIdent = sanitizeIdent(baseNameNoExt(inputPath));
|
||||||
std::string cpp = codegen.generate(nodes, std::string("compiled from ") + inputPath, shared, symbol);
|
std::string symbol = "jetc_render_" + baseIdent;
|
||||||
|
std::string apiSymbol = "jetc_api_" + baseIdent;
|
||||||
|
if (!apiRoutes.empty() && !shared)
|
||||||
|
std::cerr << "jetc: warning: #api directive(s) present but --shared not specified - "
|
||||||
|
"route metadata will not be emitted" << std::endl;
|
||||||
|
std::string cpp = codegen.generate(nodes, std::string("compiled from ") + inputPath, shared,
|
||||||
|
symbol, apiRoutes, apiSymbol);
|
||||||
std::ofstream outFile(outputPath);
|
std::ofstream outFile(outputPath);
|
||||||
outFile << cpp;
|
outFile << cpp;
|
||||||
outFile.close();
|
outFile.close();
|
||||||
if (shared)
|
if (shared) {
|
||||||
std::cout << "jetc: wrote " << outputPath << " (entry symbol: " << symbol << ")" << std::endl;
|
std::cout << "jetc: wrote " << outputPath << " (entry symbol: " << symbol << ")";
|
||||||
else
|
if (!apiRoutes.empty())
|
||||||
|
std::cout << " (api symbol: " << apiSymbol << ", " << apiRoutes.size() << " route(s))";
|
||||||
|
std::cout << std::endl;
|
||||||
|
} else {
|
||||||
std::cout << "jetc: wrote " << outputPath << std::endl;
|
std::cout << "jetc: wrote " << outputPath << std::endl;
|
||||||
|
}
|
||||||
} catch (std::exception &e) {
|
} catch (std::exception &e) {
|
||||||
std::cerr << "jetc: " << e.what() << std::endl;
|
std::cerr << "jetc: " << e.what() << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
Reference in new issue
Block a user