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
+}