02-Loops and if.md
03-Functions and scopes.md
04-Structs.md
05-Imports and namespaces.md
06-Enums and switch.md
07-Debugging.md
08-Macros and preprocessor.md
10-Annotations.md
11-Operator overloading.md
15-Polymorphism.md
16-Maps and arrays.md
17-Constructors and destructors.md
18-Platforms.md
19-Testing.md
20-Type information.md
21-Inline assembly.md
22-Libraries.md
23-Style conventions.md
24-Exceptions.md
25-Bytecode and VM.md
26-Targeting ARM.md
27-Compile time execution.md
28-Compiler functions.md
29-Importing C headers.md
100-Standard libraries.md
101-OpenSSL.md
102-Logger.md
README.md
22-Libraries.md
For quick reference:
- Compile dynamic library:
btb main.btb -o main.dll (or-o main.so ) - Compile static library:
btb main.btb -o main.lib (or-o libmain.a ) - Compile executable:
btb main.btb -o main.exe (or-o main )
Linking with libraries
Libraries to link with are specified in the source code of the program using theFunctions from a library are marked with
Linking with a dynamic library
Let's say this is your project structure. You want to compile two dynamic libraries, sound.so and broadcast.so, which should should be linked when compiling the main program.
project
|-main.btb
|-sound.c
|-broadcast.btb
// btb main.btb --run
// Tell the compiler to link with 'libs/sound.lib' and let functions
// refer to this library as 'Sound'.
#load "./libsound.so" as Sound
#load "./libbroadcast.so" as Broadcast
// @import tells the compiler that this function comes from a library
// and shouldn't have a body.
fn @import(Sound) PlaySoundFromFile(path: char*);
fn @import(Broadcast) BroadcastMsg(text: char*);
PlaySoundFromFile("theme.mp3".ptr)
BroadcastMsg("main is done".ptr)
// gcc sound.c -fpic -shared -o libsound.so
#include <stdio.h>
void PlaySoundFromFile(const char* path){
printf("Play sound: %s\n", path);
}
broadcast.btb
// btb broadcast.btb -o libbroadcast.so
#import "Logger"
fn @export BroadcastMsg(text: char*){
log("Broadcast: ", text);
}
Compile and run the files with these commands.
gcc sound.c -fpic -shared -o libsound.so
btb broadcast.btb -o libbroadcast.so
btb main.btb -o main
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
./main
tells Linux to search current working directory for libsound.so when running
Other information
You can use an alias for the imported/exported function like this:
fn @import(Sound, alias="PlaySoundFromFile") do_sound(path: char*);
fn @export(Sound, alias="btb_sendmsg") sendmsg(path: char*) { /* ... */ }
Exported functions cannot be polymorphic. You cannot export functions with the same name.
You can use
#link "nostdlib"
exists to link with libraries independently from the linker. The compiler uses the right compiler options depending on the linker.
The path specified in the
- Relative to current working directory
- Absolute path
- Relative to the directory of the compiler executable
- Relative to import directories (specified through arguments to the compiler)
- A path without a slash is assumes to be a system library.
#load "lib_file" ->gcc -llib_file . Use#load "./lib_file" for relative path.
When importing (or exporting) the calling convention defaults to
Auto-generated declarations
When compiling a library, a
// math.btb
fn @export add(x: i32, y: f32) -> i32 {
return x + y
}
// app.btb
#import "math_decl.btb"
#import "Logger"
add(9,10)
// math_decl.btb (auto-generated)
#load "math.lib" as math
fn @import(math) add(x: i32, y: f32) -> i32;
Assuming the two files above is in the same directory you can run
If you take a look at math_decl.btb you will see a *
load* directive at the top. The path is based on the relative path to the library. btb math.btb -o libs/math.lib will result in #load "./libs/math.lib" as math . The name of the library (math) is derived from the output path. btb math.btb -o MyMaTH.lib results in #load "MyMaTH.lib" as MyMaTH .
The compiler will also generate - Exported functions with betcall calling convention will be skipped.
- Polymoprhic struct types will "specialized" (Array
-> Array_i32).
What does @import do and how does the compiler link stuff?
TODO: Write some text about Linux. TODO: Include resources (websites) that contains more information about each part.
James McNellis "Everything You Ever Wanted to Know about DLLs"
Before we begin it is important to know that a library linked with one compiler tool chain (MSVC, GCC) will be compatible with another. When testing things you must make sure to recompile every library when changing which linker you are using. Therefore, you should probably not change the linker (GCC is recommended because the compiler doesn't support debug information with MSVC). Also, the text is based on Windows and x64. Things described may not be true on Linux.
This section covers details which is useful to know if your program is crashing or you have linker errors. Compile these files and follow along the explanation to better understand linking:
sound.btb
// Compile with:
// btb sound.btb -o sound.dll
// btb sound.btb -o sound.lib
#import "Logger"
#if BUILD_DLL
fn @export Play_dll(path: char*) {
log("dll - Play sound: ", path)
}
#else
fn @export Play_lib(path: char*) {
log("lib - Play sound: ", path)
}
#endif
test_link.btb
// Compiler and run with:
// btb test_link.btb -o test_link.exe -r
#load "./sound.dll" as Sound_dll
#load "./sound.lib" as Sound_lib
fn @import(Sound_dll) Play_dll(path: char*);
fn @import(Sound_lib) Play_lib(path: char*);
fn main() {
Play_lib("hi.wav".ptr)
Play_dll("hi.wav".ptr)
}
First, let's figure out what we are dealing with. When we link the final executable
Before we move on it is important to remember that different compilers do things differently. For example, you cannot link dlls to an executable using MSVC linker. There you have to link with a static import library. When compiling
// Linker command when using MSVC
link /nologo /INCREMENTAL:NO /entry:main /DEFAULTLIB:MSVCRT bin/test_link.o Kernel32.lib sound.lib sounddll.lib /OUT:test_link.exe
// Command when using gcc
gcc bin/test_link.o -lKernel32 sound.lib sound.dll -o test_link.exe
The executable contains three things (not literally). The x64 code for
The x64 code for calling a function from the static library is very simple. It is a
To understand the purpose of the Import Address Table we must first understand the usage of dynamic libraries. Dynamic libraries can be loaded and called programmatically using functions from the operating system. There are usually three functions: loading a dynamic library, acquiring a pointer to a function, and freeing the library. Below is some code that does this, I urge you to test it yourself.
#import "OS" // imports LoadDynamicLibrary and the struct DynamicLibrary
// On windows, LoadDynamicLibrary = LoadLibrary and DynamicLibrary.get_pointer = GetProcAddress
fn main() {
dll := LoadDynamicLibrary("sound.dll")
ptr := dll.get_pointer("Play_dll") // Play_dll can alternatively be a global variable, it's not limited to functions
// When we acquire the pointer we don't know which type it is
// so void* is assumed.
// To call the function we must cast it to the correct function type.
// We use @oscall because when we export sound.dll and use @export on Play_dll,
// the calling convention defaults to @oscall (stdcall on Windows, sys V abi on Linux)
Play_dll := cast_unsafe< fn @oscall (char*) > ptr
Play_dll("it works".ptr)
dll.cleanup() // FreeLibrary on Windows
}
As you might have noticed, you can still link with dlls without writing this code. That is because the linker does it for you by creating an Import Address Table (IAT), loading dynamic libraries, and filling the table with pointers when the executable starts. This is what you call load-time dynamic linking (Microsoft).
Now we can answer the question of how we call
With all this knowledge we can write a program in assembly.
// Compile with:
// btb test_link.btb -o test_link.exe -r
// The compiler doesn't know that the assembly uses
// these libraries so we must forcefully link them
#load @force "./sound.dll" as Sound_dll
#load @force "./sound.lib" as Sound_lib
fn @import(Sound_dll) Play_dll(path: char*);
fn @import(Sound_lib) Play_lib(path: char*);
fn main() {
asm("cool".ptr) {
#if !LINK_MSVC
// GCC syntax with intel flavour
// declare external symbols
.extern Play_lib
.extern __imp_Play_dll
// NOTE: We assume stdcall convention
pop rbx // pop pointer to string which was passed to inline assembly
sub rsp, 32 // alloc space for args, stdcall needs 32 bytes
mov rcx, rbx // Set first argument
call Play_lib
mov rcx, rbx // Set first argument again because rcx is a volatile register while rbx isn't
call qword ptr [rip+__imp_Play_dll]
// rip+ tells the assembler to use rip relative instruction
// We get the wrong instruction and relocation without it
add rsp, 32 // free space for args
}
Play_lib("hi.wav".ptr)
Play_dll("hi.wav".ptr)
}
Same assembly with syntax for MSVC assembler (MASM)
// btb test_link.btb -o test_link.exe -r
// The compiler doesn't know that the assembly uses
// these libraries so we must forcefully link them
#load @force "./sound.dll" as Sound_dll
#load @force "./sound.lib" as Sound_lib
fn @import(Sound_dll) Play_dll(path: char*);
fn @import(Sound_lib) Play_lib(path: char*);
fn main() {
asm("cool".ptr) {
// MSVC syntax with intel flavour
extern Play_lib : proto
extern __imp_Play_dll : proto
pop rbx
sub rsp, 32
mov rcx, rbx
call Play_lib
mov rcx, rbx
call qword ptr [__imp_Play_dll]
// MSVC assembler generate the call we want, no need for rip+ (rip+ isn't even valid syntax)
add rsp, 32 // free space for args
}
Play_lib("hi.wav".ptr)
Play_dll("hi.wav".ptr)
}
NOTE: There is an extra thing that linkers do which is stubs for functions from dynamic libraries. These are created if the compiler created a
NOTE: I have not explained the various relocation types and symbols because I barely now what the different types do myself. I have managed to make things work by analyzing the object files created by gcc and msvc but I plan to take some time to better understand the relocation types.
Experimental features
Non-repetitive library import
If you have a library with many functions which you want to import like GLAD or GLFW it would be nice if you didn't have to use @import(Math) on every single function. Below are some examples:
#load "math.lib" as Math
// Everything in the namespace receives @import(Math) to all functions within the scope
namespace @import(Math) {
fn box_inside_box(...) -> bool;
fn circle_inside_box(...) -> bool;
}
// A general form where any annotation can be applied to all functions.
#apply_annotation @import(Math) @betcall {
fn box_inside_box(...) -> bool;
fn circle_inside_box(...) -> bool;
}
Even though that's great, we still have a problem with alias. What if every function is prefixed with
#apply_annotation @import(Math, alias="math_*") { /* ... */ }
#apply_annotation @import(Math, prefix_alias="math_") { /* ... */ }