diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3e6484..9d05a54 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,8 @@ jobs: build/finch version - name: Tests run: tests/run.sh + - name: Shared library + run: tests/shared.sh - name: Tests under valgrind if: matrix.arch == 'x64' run: MEMCHECK=1 tests/run.sh diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md index a9f7408..42dfe2a 100644 --- a/docs/en/cheatsheet.md +++ b/docs/en/cheatsheet.md @@ -12,6 +12,7 @@ Everything in Finch on one page. For explanations, see the [guides](../../README |---|---| | `finch run file.fch [args…]` | compile and run (args go to the program) | | `finch build file.fch [-o name]` | make a program (`name.exe` on Windows) | +| `finch build file.fch --shared` | make a library (`libfile.so`, `file.dll`) that a C program loads; its `export fn`s are its C functions (numbers, bool, char, pointers) | | `finch ir file.fch` | print the LLVM IR | | `finch lsp` | the language server (editors use it) | | `finch version` | version | diff --git a/runtime/finch_rt.c b/runtime/finch_rt.c index 4cb2d13..d825f64 100644 --- a/runtime/finch_rt.c +++ b/runtime/finch_rt.c @@ -37,12 +37,26 @@ static char empty[1] = ""; // ---------- runtime errors ---------- -__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) { +// A program that loads Finch libraries (finch build --shared) can take runtime errors over +// instead of the process exiting: finch_module_on_fatal(handler) in each library. The handler +// gets the message and must not return (longjmp out, or exit itself). +static void (*fatal_hook)(const char *msg); +void finch_on_fatal(void (*hook)(const char *msg)) { fatal_hook = hook; } + +static __attribute__((noreturn)) void fatal(const char *msg) { + if (fatal_hook) fatal_hook(msg); fflush(stdout); - fprintf(stderr, "%s:%lld: runtime error: %s\n", file, (long long)line, msg); + fprintf(stderr, "%s\n", msg); exit(1); } +__attribute__((noreturn, cold)) void finch_panic(const char *file, int64_t line, const char *msg) { + char full[1024]; + snprintf(full, sizeof full, "%s:%lld: runtime error: %s", file, (long long)line, msg); + fflush(stdout); + fatal(full); +} + __attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t line, int64_t i, int64_t len) { char msg[128]; snprintf(msg, sizeof msg, "index %lld is out of range (the length is %lld)", (long long)i, (long long)len); @@ -52,8 +66,7 @@ __attribute__((noreturn, cold)) void finch_panic_index(const char *file, int64_t static void *must(void *p) { if (!p) { fflush(stdout); - fprintf(stderr, "runtime error: out of memory\n"); - exit(1); + fatal("runtime error: out of memory"); } return p; } diff --git a/src/ast.h b/src/ast.h index 87c3828..9651207 100644 --- a/src/ast.h +++ b/src/ast.h @@ -315,6 +315,7 @@ struct FnDecl { Pos recvPos; Type ret; // Void when there is no `-> type` bool fallible = false; // -> int! / -> ! : can fail with an error + bool exported = false; // export fn: callable from C by its own name (a library's entry point) std::vector params; BlockPtr body; }; diff --git a/src/codegen.cpp b/src/codegen.cpp index e0b4906..01241b6 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -4,6 +4,7 @@ #include "codegen_impl.h" #include "builtins_doc.h" +#include "target.h" using namespace llvm; @@ -36,12 +37,29 @@ Codegen::Codegen(LLVMContext &c, const std::vector &ps, const CImports std::unique_ptr Codegen::run() { declareStructs(); declareFns(); - if (!modules[""].fns.count("main")) failAt(0, 1, 1, "there is no main function (add 'fn main() { ... }')"); + if (g_target.shared) { + if (modules[""].fns.count("main")) { + const Pos &pos = modules[""].fns["main"].decl->namePos; + failAt(pos.file, pos.line, pos.col, "a library (--shared) has no main: the program that loads it calls its 'export fn's"); + } + } else if (!modules[""].fns.count("main")) { + failAt(0, 1, 1, "there is no main function (add 'fn main() { ... }')"); + } for (auto &[name, m] : modules) for (auto &[fname, fn] : m.fns) define(fn); for (auto &[s, ms] : methods) for (auto &[mname, fn] : ms) define(fn); - defineMainWrapper(); + if (!g_target.shared) defineMainWrapper(); + for (const auto &[module, name] : exportNames) defineExport(modules[module].fns[name]); + if (g_target.shared) { // finch_module_on_fatal(handler): the host's say on runtime errors (see finch_rt.c) + Function *w = Function::Create(FunctionType::get(b.getVoidTy(), {b.getPtrTy()}, false), Function::ExternalLinkage, + "finch_module_on_fatal", mod.get()); + if (g_target.windows) w->setDLLStorageClass(GlobalValue::DLLExportStorageClass); + b.SetInsertPoint(BasicBlock::Create(ctx, "entry", w)); + b.SetCurrentDebugLocation(DebugLoc()); + b.CreateCall(rt("finch_on_fatal"), {w->getArg(0)}); + b.CreateRetVoid(); + } if (di) di->finalize(); std::string err; @@ -188,8 +206,44 @@ void Codegen::declare(const Program &p, const FnDecl &fn) { // User functions get a prefix so they never clash with C library names. std::string name = "finch." + (p.module.empty() ? "" : p.module + ".") + (recv ? recv->name + "." : "") + fn.name; f.llvm = Function::Create(FunctionType::get(retType(f), params, false), Function::InternalLinkage, name, mod.get()); + if (fn.exported) { + if (recv) failAt(pos.file, pos.line, pos.col, "methods can't be exported (export a plain fn that calls it)"); + if (f.fallible) failAt(pos.file, pos.line, pos.col, "an export fn can't fail (C has no errors as values): handle them inside it"); + auto cType = [](const FType &t) { return t.isNumber() || t.kind == FType::Bool || t.kind == FType::Char || t.kind == FType::Ptr; }; + for (size_t i = 0; i < f.params.size(); i++) + if (!cType(f.params[i])) + failAt(fn.params[i].pos.file, fn.params[i].pos.line, fn.params[i].pos.col, + "an export fn takes numbers, bool, char and pointers only (this is " + f.params[i].show() + + "; pass text as ptr[char] and make it a str inside)"); + if (f.ret.kind != FType::Void && !cType(f.ret)) + failAt(pos.file, pos.line, pos.col, "an export fn returns nothing, a number, bool, char or a pointer (not " + f.ret.show() + ")"); + for (const auto &[mname, other] : modules) + if (const auto it = other.fns.find(fn.name); it != other.fns.end() && it->second.decl->exported) + failAt(pos.file, pos.line, pos.col, "'" + fn.name + "' is exported twice (from two modules)"); + } if (recv) methods[recv][fn.name] = f; else m.fns[fn.name] = f; + if (fn.exported) exportNames.push_back({p.module, fn.name}); +} + +// export fn name(...): a C function called `name` that calls the Finch one (bool is a C byte). +void Codegen::defineExport(Fn &fn) { + auto cTy = [&](const FType &t) -> llvm::Type * { return t.kind == FType::Bool ? b.getInt8Ty() : ty(t); }; + std::vector params; + for (const FType &t : fn.params) params.push_back(cTy(t)); + llvm::Type *ret = fn.ret.kind == FType::Void ? b.getVoidTy() : cTy(fn.ret); + Function *w = Function::Create(FunctionType::get(ret, params, false), Function::ExternalLinkage, fn.decl->name, mod.get()); + if (g_target.windows) w->setDLLStorageClass(GlobalValue::DLLExportStorageClass); + b.SetInsertPoint(BasicBlock::Create(ctx, "entry", w)); + b.SetCurrentDebugLocation(DebugLoc()); + std::vector args; + for (size_t i = 0; i < fn.params.size(); i++) { + Value *a = w->getArg(unsigned(i)); + args.push_back(fn.params[i].kind == FType::Bool ? b.CreateICmpNE(a, b.getInt8(0)) : a); + } + Value *r = b.CreateCall(fn.llvm, args); + if (fn.ret.kind == FType::Void) b.CreateRetVoid(); + else b.CreateRet(fn.ret.kind == FType::Bool ? b.CreateZExt(r, b.getInt8Ty()) : r); } // The real C `main` calls the Finch one and turns its result into an exit code. diff --git a/src/codegen_builtins.cpp b/src/codegen_builtins.cpp index 649a22b..bfec91f 100644 --- a/src/codegen_builtins.cpp +++ b/src/codegen_builtins.cpp @@ -50,6 +50,7 @@ Function *Codegen::rt(const std::string &name) { struct Sig { llvm::Type *ret; std::vector params; }; std::map sigs = { {"finch_panic", {V, {P, I, P}}}, + {"finch_on_fatal", {V, {P}}}, {"finch_panic_index", {V, {P, I, I, I}}}, {"finch_alloc", {P, {I}}}, {"finch_zalloc", {P, {I}}}, diff --git a/src/codegen_impl.h b/src/codegen_impl.h index 4d3b8f8..4d838c4 100644 --- a/src/codegen_impl.h +++ b/src/codegen_impl.h @@ -174,6 +174,8 @@ public: void declare(const Program &p, const FnDecl &fn); void define(Fn &fn); void defineMainWrapper(); + void defineExport(Fn &fn); + std::vector> exportNames; // (module, name) of each export fn void block(const BlockStmt &blk); void blockBody(const BlockStmt &blk); void stmt(const Stmt &s); diff --git a/src/lexer.cpp b/src/lexer.cpp index ffe367f..fc7ea12 100644 --- a/src/lexer.cpp +++ b/src/lexer.cpp @@ -5,7 +5,7 @@ #include static const std::unordered_map keywords = { - {"fn", Tok::Fn}, {"return", Tok::Return}, {"if", Tok::If}, + {"fn", Tok::Fn}, {"export", Tok::Export}, {"return", Tok::Return}, {"if", Tok::If}, {"else", Tok::Else}, {"while", Tok::While}, {"for", Tok::For}, {"in", Tok::In}, {"break", Tok::Break}, {"continue", Tok::Continue}, {"true", Tok::True}, {"false", Tok::False}, {"null", Tok::Null}, @@ -154,6 +154,7 @@ const char *tokName(Tok t) { case Tok::String: return "a string"; case Tok::Char: return "a char"; case Tok::Fn: return "'fn'"; + case Tok::Export: return "'export'"; case Tok::Return: return "'return'"; case Tok::If: return "'if'"; case Tok::Else: return "'else'"; diff --git a/src/lexer.h b/src/lexer.h index 23761b8..491f064 100644 --- a/src/lexer.h +++ b/src/lexer.h @@ -8,7 +8,7 @@ enum class Tok { Ident, Int, Float, String, Char, // keywords - Fn, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer, + Fn, Export, Return, If, Else, While, For, In, Break, Continue, True, False, Null, Import, Link, Struct, Defer, OrElse, Try, // or, try // punctuation diff --git a/src/main.cpp b/src/main.cpp index 73ec72c..fe8f2f0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -42,6 +42,7 @@ static void usage() { " finch lsp the language server, for editors (VS Code, Kate, ...)\n" "\n" " -l link a C library, same as link \"lib\" in the file\n" + " --shared (build) a library with no main: lib.so / .dll, its export fn's callable from C\n" " --target build for another system: windows, linux, arm64 (Linux), macos,\n" " or an LLVM triple\n" " -g add debug info (gdb / lldb / Visual Studio)\n" @@ -230,6 +231,7 @@ static bool endsWith(const std::string &s, const char *x) { static void link(const std::string &obj, const std::string &exe, const std::vector &libs, const CImports &c) { std::string cmd = g_target.cc + " " + shellQuote(obj) + " " + shellQuote(runtimeObject()) + " -o " + shellQuote(exe); + if (g_target.shared) cmd += g_target.darwin ? " -dynamiclib" : " -shared"; if (!g_target.msvc) cmd += " -lm"; std::vector temps; for (const std::string &l : libs) { @@ -289,6 +291,7 @@ int main(int argc, char **argv) { else if (a == "--target" && i + 1 < argc) targetName = argv[++i]; else if (a == "-O0") optimizeCode = false; else if (a == "-g") debug = true; + else if (a == "--shared" && cmd == "build") g_target.shared = true; else if (a == "-l" && i + 1 < argc) libs.push_back(argv[++i]); else if (a.rfind("-l", 0) == 0 && a.size() > 2) libs.push_back(a.substr(2)); else if (cmd == "run") programArgs.push_back(a); @@ -296,7 +299,8 @@ int main(int argc, char **argv) { } setTarget(targetName); - if (!outGiven) out += g_target.exe; + if (!outGiven) out = g_target.shared ? (g_target.windows ? out + ".dll" : "lib" + out + (g_target.darwin ? ".dylib" : ".so")) + : out + g_target.exe; Loader loader; loader.load(file, "", Pos{}); diff --git a/src/parser.cpp b/src/parser.cpp index 4a76616..a276cce 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -180,9 +180,10 @@ private: FnDecl function() { FnDecl fn; fn.pos = pos(); + fn.exported = accept(Tok::Export); if (at(Tok::Ident) && isTypeName(cur().text)) fail(fn.pos.line, fn.pos.col, "functions start with 'fn', like: fn add(int a, int b) -> int { ... }"); - expect(Tok::Fn, "'fn', 'struct', 'import' or 'link' (every function starts with fn)"); + expect(Tok::Fn, fn.exported ? "'fn' (export fn name(...))" : "'fn', 'struct', 'import' or 'link' (every function starts with fn)"); fn.namePos = pos(); fn.name = expect(Tok::Ident, "a function name").text; diff --git a/src/target.h b/src/target.h index eb18c34..115eab1 100644 --- a/src/target.h +++ b/src/target.h @@ -14,6 +14,7 @@ struct TargetInfo { bool cross = false; // compiling for another system than the one we run on std::string cc; // the C compiler that links programs and builds the runtime std::string exe; // ".exe" or "" + bool shared = false; // finch build --shared: a library (.so / .dll / .dylib) with no main }; inline TargetInfo g_target; diff --git a/tests/shared.sh b/tests/shared.sh new file mode 100755 index 0000000..83ea020 --- /dev/null +++ b/tests/shared.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# finch build --shared: a library a C program loads, calls by name, and that hands runtime +# errors to the host instead of exiting. +set -e +cd "$(dirname "$0")/shared" +FINCH=${FINCH:-../../build/finch} +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +"$FINCH" build mod.fch --shared -o "$tmp/libmod.so" +${CC:-cc} host.c -rdynamic -o "$tmp/host" -ldl +out=$("$tmp/host" "$tmp/libmod.so") +expected='add 42 +even 1 0 +host_print: hello Rakya +caught: mod.fch:17: runtime error: index 7 is out of range (the length is 3) +still alive' +if [ "$out" = "$expected" ]; then echo "shared library: passed"; else printf 'shared library: FAILED\n%s\n' "$out"; exit 1; fi diff --git a/tests/shared/host.c b/tests/shared/host.c new file mode 100644 index 0000000..9c5dfbb --- /dev/null +++ b/tests/shared/host.c @@ -0,0 +1,17 @@ +#include +#include +#include +static jmp_buf jb; +void host_print(const char *s) { printf("host_print: %s\n", s); } +static void onFatal(const char *m) { printf("caught: %s\n", m); longjmp(jb, 1); } +int main(int argc, char **argv) { + void *h = dlopen(argc > 1 ? argv[1] : "./libmod.so", RTLD_NOW | RTLD_LOCAL); + if (!h) { printf("%s\n", dlerror()); return 1; } + ((void (*)(void (*)(const char *)))dlsym(h, "finch_module_on_fatal"))(onFatal); + printf("add %ld\n", ((long (*)(long, long))dlsym(h, "add"))(2, 40)); + printf("even %d %d\n", ((_Bool (*)(long))dlsym(h, "is_even"))(4), ((_Bool (*)(long))dlsym(h, "is_even"))(5)); + ((void (*)(const char *))dlsym(h, "hello"))("Rakya"); + if (!setjmp(jb)) ((long (*)(long))dlsym(h, "crash"))(7); + printf("still alive\n"); + return 0; +} diff --git a/tests/shared/host.h b/tests/shared/host.h new file mode 100644 index 0000000..13358a4 --- /dev/null +++ b/tests/shared/host.h @@ -0,0 +1 @@ +void host_print(const char *s); diff --git a/tests/shared/mod.fch b/tests/shared/mod.fch new file mode 100644 index 0000000..19cb22a --- /dev/null +++ b/tests/shared/mod.fch @@ -0,0 +1,22 @@ +import "host.h" + +fn greet(str who) -> str { + return "hello " + who +} + +export fn add(int a, int b) -> int { + return a + b +} + +export fn hello(ptr[char] name) { + host_print(greet(str(name))) +} + +export fn crash(int i) -> int { + xs := [1, 2, 3] + return xs[i] +} + +export fn is_even(int n) -> bool { + return n % 2 == 0 +}