4.2 KiB
Tomo Library/Module Design
There are two ways to "import" code that is defined elsewhere: local files from the same project and shared library objects from another project. The first type of import (local files) is necessary for splitting large projects into smaller components for ease of understanding and compilation speed. The second type of import (shared libraries) is to allow you to install third party libraries or frameworks that can be used across many projects.
Local Imports
To see how local imports work, let's look at a simple file:
// File: foo.tm
my_variable := "hello"
When this file is compiled to a static object file by tomo -c foo.tm
, it
produces the following C header file and C source file:
// File: foo.tm.h
#pragma once
#include <tomo/tomo.h>
extern Text_t foo$my_variable;
// File: foo.tm.c
#include <tomo/tomo.h>
#include "foo.tm.h"
Text_t foo$my_variable = "hello";
Notice that the symbols defined here (foo$my_variable
) use a file-based
prefix that includes a dollar sign. C compilers support an extension that
allows dollar signs in identifiers, and this allows us to use guaranteed-unique
prefixes so symbols from one file don't have naming collisions with symbols
in another file.
The C file is compiled by invoking the C compiler with something like: cc <flags...> -c foo.tm.c -o foo.tm.o
Now, what happens if we want to use the compiled object file?
// File: baz.tm
foo := use ./foo.tm
func say_stuff():
say("I got {foo.my_variable} from foo")
func main():
say_stuff()
If I want to run baz.tm
with tomo baz.tm
then this transpiles to:
// File: baz.tm.h
#pragma once
#include <tomo/tomo.h>
#include "./foo.tm.h"
void baz$say_stuff();
void baz$main();
// File: baz.tm.c
#include <tomo/tomo.h>
#include "baz.tm.h"
public void baz$say_stuff()
{
say(Texts(Text("I got "), foo$my_variable, Text(" from foo")));
}
public void baz$main()
{
baz$say_stuff();
}
Then baz.tm.o
is compiled to a static object with cc <flags...> -c baz.tm.c -o baz.tm.o
.
Next, we need to create an actual executable file that will invoke baz$main()
(with any command line arguments). To do that, we create a small wrapper
program:
// File: /tmp/program.c
#include <tomo/tomo.h>
#include "baz.tm.h"
int main(int argc, char *argv[])
{
tomo_init();
if (argc > 1)
errx(1, "This program doesn't take any arguments.");
baz$main();
return 0;
}
This program is compiled with the already-built object files to produce an
executable binary called foo
like this: cc <flags...> /tmp/program.c foo.tm.o baz.tm.o -o baz
Finally, the resulting binary can be executed to actually run the program!
Shared Library Imports
In Tomo, a shared library is built out of a directory that contains multiple
.tm
files. Each .tm
file in the directory (excluding those that start with
an underscore) will be compiled and linked together to produce a single
libwhatever.so
file and whatever.h
file that can be used by other Tomo
projects.
Symbol Uniqueness
In the future, each of these symbols will be given an extra prefix to prevent
namespace collisions and a standalone header file will be built that defines
every public symbol in the library in a way that a C compiler can understand.
In our example, running tomo -s=qux.1.2.3 foo.tm baz.tm
would produce a
header file like this:
#pragma once
#include <tomo.h>
extern Text_t qux$1$2$3$foo$my_variable;
extern void qux$1$2$3$baz$say_stuff();
Installing
Once the libwhatever.so
and whatever.h
files have been built, Tomo will ask
if you want to install this library. If you choose to install it, Tomo will
copy the entire directory (excluding files and directories that begin with .
such as .git
) into ~/.local/share/tomo/installed/
.
Using Shared Libraries
To use a shared library, write a statement like use qux
with an unqualified
name (i.e. not an absolute or relative path like /qux
or ./qux
). When a
program uses a shared library, that shared library gets dynamically linked to
the executable when compiling, and all of the necessary symbol information is
read from the source files during compilation.