Skip to content

Instantly share code, notes, and snippets.

@CherryDT
Last active February 13, 2022 00:16
Show Gist options
  • Save CherryDT/8e8c10c7139179774c2e6dff88433559 to your computer and use it in GitHub Desktop.
Save CherryDT/8e8c10c7139179774c2e6dff88433559 to your computer and use it in GitHub Desktop.
Using modules in FreeBasic

How compilation works

You are probably used to compiling your main.bas file with fbc main.bas, creating a file main.exe. This is an oversimplified view however. Let's see what the docs have to say about that:

At its simplest, fbc takes a source file as a command-line argument and produces an executable file. It does this by compiling the source file (.bas) into an assembly (.asm) file, then compiling this into an object file (.o) using GAS and finally linking using LD this object file to other object files and libraries it needs to run, producing the final executable file. The assembly and compiled object files are deleted at this point by default.

So, actually, this is a multistep process:

  1. main.bas is turned into a generated assembly code file main.asm (or with -gen gcc it would be a generated C code file main.c). We can execute that step alone using fbc -r main.bas.
  2. main.asm is compiled (using GAS, the assembler) to an object file main.o. We can execute that step alone using as test.asm -o test.o, or step 1 and 2 together with fbc -c main.bas.
  3. main.o is linked (using LD, the linker), together with the default static libraries and other object files that FreeBasic requires (for example fbrt0.o), to an executable file main.exe. We could do this step alone using a command with ld -o main.exe main.o <all the libraries here> but it'd be quite a complex command. We can also do steps 2 & 3 together with simply fbc main.o.

An object file is a compiled file that contains machine code but has missing links in various locations - kind of like pointers that don't have a target yet. These are the references to "symbols", i.e. named addresses. Instead of having the final address of some function, they only have the internal name of that function, and it's the linker's job at the end to resolve those references and put the final addresses into the code before joining all the different parts of machine code into a final executable.

We can see all that happening if we call fbc with the verbose argument -v:

C:\Users\david\Temp\test1>fbc -v main.bas
FreeBASIC Compiler - Version 1.03.0 (04-12-2015), built for win32 (32bit)
Copyright (C) 2004-2015 The FreeBASIC development team.
standalone
target:       win32, 486, 32bit
compiling:    main.bas -o main.asm (main module)
assembling:   z:\programme\fb\bin\win32\as.exe --32 --strip-local-absolute "main.asm" -o "main.o"
linking:      z:\programme\fb\bin\win32\ld.exe -m i386pe -o "main.exe" -subsystem console "z:\programme\fb\lib\win32\fbextra.x" --stack 1048576,1048576 -s -L "z:\programme\fb\lib\win32" -L "." "z:\programme\fb\lib\win32\crt2.o" "z:\programme\fb\lib\win32\crtbegin.o" "z:\programme\fb\lib\win32\fbrt0.o" "main.o" "-(" -lfb -lgcc -lmsvcrt -lkernel32 -luser32 -lmingw32 -lmingwex -lmoldname -lgcc_eh "-)" "z:\programme\fb\lib\win32\crtend.o"

More information about what object files and linking are can be found in the Wikipedia article about linkers.

Note that library files (.a) are essentially a group of "half-linked" object files which can be linked with other object files and other libraries to an executable - kind of like a ZIP file of object files (in fact the a comes from "archive" for that reason).

Compiling modules

Let's go back to the docs we looked at before and continue with the next paragraph:

fbc can accept multiple source files at once, compile and link them all into one executable. For example, the following command,

fbc foo.bas bar.bas baz.bas

produces the executable foo.exe in DOS and Windows, and ./foo in Linux. Since foo.bas was listed first, it will be the main entry point into the executable, and also provide its name.

What this does is essentially the following:

  1. For each of the three files, steps 1 & 2 of the compilation process are executed. So, foo.bas is turned into foo.asm and then foo.o, the same for bar.bas and baz.bas.
  2. In step 3 of the compilation process, the linking, all of the three object files foo.o, bar.o and baz.o are linked together into foo.exe.

We can also separate these steps manually:

  1. fbc -c foo.bas bar.bas baz.bas -m foo (the -m foo specifies foo as main module - this is required because with -c fbc won't automatically use the first module as main module, and only the main module will contain the entry point function WinMain@16 that Windows expects).
  2. fbc foo.o bar.o baz.o - will produce foo.exe.

Let's try this:

main.bas

Print "Hello from Main!"

module2.bas

Print "Hello from Module 2!"
> fbc main.bas module2.bas

> main.exe

Hello from Module 2!
Hello from Main!

As you can see, both source files are compiled together to one executable file.

You may be surprised to see Hello from Module 2! printed before Hello from Main!, but it will start making sense if you think about the fact that you would usually have the actual start of the code in your main module and the other modules would just initialize themselves as necessary, so you'd probably want those modules to be initialized before the main code starts, so their top-level code actually executes first.

We can use -v again to see exactly what's happening:

C:\Users\david\Temp\test1>fbc -v main.bas module2.bas
FreeBASIC Compiler - Version 1.03.0 (04-12-2015), built for win32 (32bit)
Copyright (C) 2004-2015 The FreeBASIC development team.
standalone
target:       win32, 486, 32bit
compiling:    main.bas -o main.asm (main module)
compiling:    module2.bas -o module2.asm
assembling:   z:\programme\fb\bin\win32\as.exe --32 --strip-local-absolute "main.asm" -o "main.o"
assembling:   z:\programme\fb\bin\win32\as.exe --32 --strip-local-absolute "module2.asm" -o "module2.o"
linking:      z:\programme\fb\bin\win32\ld.exe -m i386pe -o "main.exe" -subsystem console "z:\programme\fb\lib\win32\fbextra.x" --stack 1048576,1048576 -s -L "z:\programme\fb\lib\win32" -L "." "z:\programme\fb\lib\win32\crt2.o" "z:\programme\fb\lib\win32\crtbegin.o" "z:\programme\fb\lib\win32\fbrt0.o" "main.o" "module2.o" "-(" -lfb -lgcc -lmsvcrt -lkernel32 -luser32 -lmingw32 -lmingwex -lmoldname -lgcc_eh "-)" "z:\programme\fb\lib\win32\crtend.o"

Now let's try something else. Let's create a constant MyName in each module with Main and Module 2 in it respectively and print that:

main.bas

Const MyName = "Main"
Print "Hello from " & MyName & "!"

module2.bas

Const MyName = "Module 2"
Print "Hello from " & MyName & "!"

If we compile this again with fbc main.bas module2.bas and run it, we'll see it still works. But why does this work, shouldn't we get an error Duplicated definition, MyName in 'Const MyName = "Main"'?

The reason we don't get such an error is because the two source files are compiled separately - they are separate "compilation units". Only the already-compiled object files are then linked together.

This is different from using for example #Include to add the contents of one file into the other via the preprocessor (you can use the -pp flag to see the resulting file - it will be essentially both source files concatenated) because in that case the whole code is compiled as a single compilation unit.

The benefits are mainly that one module won't step on another module's toes with naming conflicts* and that a change in one module will only require recompilation of that module and not all modules. A good IDE like FBEdit or a build tool like Make will detect which source files changed since the last compilation and recompile only those when you hit "build" (followed by linking of course).

*: Note: Subs/functions are by default public and therefore can cause conflicts, but see below how to avoid that.

Sharing functions, variables and types between modules

Just having two modules that individually run code isn't tremendously useful. It becomes much more exciting if you expose certain functions or variables from one module to use in other modules.

Let's start with the naive approach of defining a function in one module and using it in another:

sum.bas

Function CalculateSum (x As Integer, y As Integer) As Integer
  Return x + y
End Function

main.bas

Dim As Integer a, b
Input "First number: ", a
Input "Second number: ", b
Print "Sum:"; CalculateSum(a, b)
> fbc main.bas sum.bas

main.bas(4) error 41: Variable not declared, CalculateSum in 'Print "Sum:"; CalculateSum(a, b)'

That didn't work. And if we try fbc sum.bas main.bas or fbc sum.bas main.bas -m main it won't work either. Why is that? Well, since main.bas and sum.bas are compiled as separate units, main.bas won't know about any function CalculateSum since it's not defined inside of its own compilation unit! That should become obvious if you think about it that you should be able to compile them on their own and later manually link them (fbc -c main.bas -m main, fbc -c sum.bas, fbc main.o sum.o - here, when we do fbc -c main.bas -m main there isn't even any reference to our sum module, so how should it know about our function?).

The solution is to declare the function in main.bas so that the compiler knows that such a function is supposed to exist, and what its parameters are. It will then happily compile main.bas to an object file with a reference to a function CalculateSum, even though that function is not defined in the same module, and it's up to the linker at the end to resolve that reference. This is the same thing as when you use any other function that's not defined in your own module - for example the FreeBasic function Chr, which is provided by the library file libfb.a at the end.

This is done using the Declare keyword. Here is the updated main.bas file:

Declare Function CalculateSum (x As Integer, y As Integer) As Integer

Dim As Integer a, b
Input "First number: ", a
Input "Second number: ", b
Print "Sum:"; CalculateSum(a, b)
> fbc main.bas sum.bas

> main.exe

First number: 2
Second number: 5
Sum: 7

Now the code compiles and works!

And, just out of curiosity, what happens if we try to compile one of the files alone, but not just to an object file but to a full executable?

> fbc sum.bas

> sum.exe

For sum.bas, it compiles, but when we run sum.exe, nothing happens - of course, since there is only a function definition in the file and no actual top-level code.

> fbc main.bas

main.o:fake:(.text+0x8d): undefined reference to `CALCULATESUM@8'

But for main.bas, it doesn't compile properly. Actually, to be exact, the compilation part (to an object file) works just fine, but the linking then fails, because main.bas is referring to a function CalculateSum that we promised (using Declare) that it is to be found somewhere eventually, but we lied - it couldn't be found at the end, therefore the linking operation fails with an undefined reference.

Now let's assume we have more modules already, and several of them want to use the CalculateSum function, or other functions provided by other modules. It will become cumbersome and error-prone to add long Declare lines with the function signature to every module that wants to use those functions. This is where header files (.bi) come into play.

If we create a file sum.bi that has the Declare line in it, then other modules only need to use #Include "sum.bi" to be able to reference the function. With a project with many modules, we can also just create one "master" header file that includes all other headers files, so every module just needs to include that master header file and can now use all the functions that other modules provide!

Let's expand this idea and create a module that exposes a UDT named Person, and have another module use it. Here, we would put the type definition for Person into a header file as well, because both person.bas and main.bas would use it, however the implementation of methods of Person would be defined in person.bas:

person.bi

Type Person
  Name As String
  Age As Integer
  
  Declare Constructor (Name As String, Age As Integer)
  Declare Sub Greet ()
End Type

person.bas

#Include "person.bi"

Constructor Person (_Name As String, _Age As Integer)
  Name = _Name
  Age = _Age
End Constructor

Sub Person.Greet ()
  Print "Hello, my name is " & Name & " and I'm " & Age & " years old!"
End Sub

main.bas

#Include "person.bi"

Var Bob = Person("Bob", 25)
Bob.Greet()
> fbc main.bas person.bas

> main.exe

Hello, my name is Bob and I'm 25 years old!

Remember when I mentioned that having different compilation units has the benefit of being able to write modular code without worrying about naming conflicts? You may notice that there was nothing I had to do in my modules to ensure that a sub/function is visible to other modules, I only had to declare it in those other modules. This is because in FreeBasic, subs and functions are public by default, i.e. can be seen by all other modules. That also means that we can have conflicts even if a function is designed only for internal use. So, what about that benefit then?

The solution is to make use of the Private keyword for subs/functions. If a function is defined like this, then it will not be visible to other modules, and hence also can't clash with them:

Private Function InternalThing () As Integer
  ' ...
End Function

Finally, let's see how it works if we have global variables that we would like to share between modules. This is done with the Common keyword:

shared.bi

Common Shared MySharedVariable As Integer

Declare Sub PrintIt ()

main.bas

#Include "shared.bi"

MySharedVariable = 42
PrintIt()

module2.bas

#Include "shared.bi"

Sub PrintIt ()
  Print "Shared variable is"; MySharedVariable
End Sub
> fbc main.bas module2.bas

> main.exe

Shared variable is 42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment