This Beginner's tutorial aims to help beginning programmers to get to know Bennu. In its current state, the tutorial is reasonably advanced already, so if you don't understand something, please leave a note. Any comments or suggestions are also welcome.
- 1 The Beginning
- 2 Variables and datatypes
- 3 Moving Up
Bennu, like every compiled language, has a compiler and an interpreter. The first one is used to convert the source file, the code that you write, to a file that the interpreter can understand. In this tutorial we are going to concentrate on the source file and move on from there.
A source file is simply a text file that respects certain syntax. This file can be created in any text editor, like the windows notepad. Bennu does not have an official IDE but there are patches and tutorials to make some free IDEs ready for Bennu (see Setting up Bennu).
For the sake of standardization there a few file extensions used primarily for Bennu sourcefiles:
- .prg: program code
- .inc: included code; code that is not meant to run stand-alone; header file (compare with C's .h).
Here's a basic source file:
import "mod_say" // import the module to output text to console, using say() Process Main() // start the definition of the main process Begin // start the code say("Hello World!"); End // end the definition of the main process
From Hello World
Introduction to processes
Like many other languages, Bennu has functions. Functions can be seen as parts of code you can run with only a single statement (the function call). The caller will wait for the function to finish. Let's clarify this with an example.
import "mod_say" // import the module to output text to console, using say() Function int SayHelloWorld() // start the definition of a function called SayHelloWorld Begin // start the code say("Hello World!"); End // end the definition of the function SayHelloWorld Process Main() // start the definition of the main process Begin // start the code SayHelloWorld(); say("Hello again!"); // AFTER SayHelloWorld() is done End // end the definition of the main process
Here, we see the caller (
Main and the called function
Now you may be wondering why there are functions and processes. Processes are almost the same as functions, but with a slight difference we will discuss shortly. Try to run the same code, but making
SayHelloWorld a process:
import "mod_say" // import the module to output text to console, using say() Process int SayHelloWorld() // start the definition of a function called SayHelloWorld Begin // start the code say("Hello World!"); End // end the definition of the function SayHelloWorld Process Main() // start the definition of the main process Begin // start the code SayHelloWorld(); say("Hello again!"); // AFTER SayHelloWorld() is done End // end the definition of the main process
You will notice it doesn't make a difference here.
More about processes
Bennu has a unique way of programming, because it makes use of processes and frames. Frames can be seen as a way of controlling the speed of your application. For example, you can tell Bennu to run at 60 frames per second, 100 frames per second or even 1000 frames per second. Of course, if the machine can't handle it, 1000 FPS won't be reached. In each frame, every process performs its code and advances a frame.
To have processes run in parallel, just call it like you would a function. When a process is called, it causes the caller to wait for the process, also like a function. However, when the process reaches a frame; statement, it tells the caller it can continue and this is where the parallelism begins. It is important to note that it is not defined in what order the processes will be run after this. In most cases it is the order in which the processes were started, but that is not guarenteerd. Let's see this in action.
import "mod_say" Process int SayHelloWorld() Begin say("SayHelloWorld: 1"); // this gets run in the first frame frame; // wait for the second frame to start say("SayHelloWorld: 2"); // this one in the second frame frame; // wait for the third frame to start say("SayHelloWorld: 3"); // this one in the third frame End Process Main() Begin SayHelloWorld(); say("Main: 1"); // this gets run in the first frame and AFTER the first one SayHelloWorld frame; // wait for the second frame to start say("Main: 2"); // this one in the second frame, BEFORE the one in SayHelloWorld frame; // wait for the third frame to start say("Main: 3"); // this one in the third frame, BEFORE the one in SayHelloWorld End
The output is:
SayHelloWorld: 1 Main: 1 Main: 2 SayHelloWorld: 2 Main: 3 SayHelloWorld: 3
In the first frame,
Main waits for
SayHelloWorld, so the
SayHelloWorld is executed before the one in
Main. After that, the processes run in turns, in this case
Maingets his turn before
SayHelloWorld. However, it is possible to influence the order of execution, but more on that later.
Bennu's interpretation works with frames. You can limit the execution rate by limiting the allowed amount of frames per second. To tell Bennu a process is done for the current frame, the statement
frame; can be used. Multiple processes will take turns in getting executed and will not be executed simultaneously. For the more advanced people reading this, this is indeed a lot like coroutines, where instead of frame, the keyword yield is used; In Bennu you can also use yield as you would use frame.
import "mod_say" import "mod_time" // import a time module, for get_timer(), which returns the number // of milliseconds after the program was started Process Main() Begin while(get_timer()<1000) say("Hello World! @" + get_timer()); frame; end End
This example will output
Hello World! @... as many times as possible in one second.
But suppose we were to limit the FPS to 25, then it would only output it 25 times, spread evenly over the second it ran. This would be pretty useful in cases. Currently, this is done by importing mod_video and using set_fps(). This means that each Bennu-frame the mod_video module 'tells' Bennu to wait until the next frame should start. For example if the FPS is 25, the time between the start of frames is 40ms. If the execution of Bennu took 10ms, mod_video will tell Bennu to wait 30ms with execution, making sure the FPS is correct.
Processes and functions
One of the key features of Bennu is the use of processes. The code in each process runs independently from each other, although never at the same time, but the execution rate is influenced by the frame statement. A frame-statement in a process says something like "this process is done for this frame" and the process will wait (at the frame-statement) for the next frame to begin and the process acquires running privileges again.
A function is almost the same, but not entirely independent. The process or function calling a function will wait for the function to return even if that function contains a frame-statement. A good example is the textinput tutorial.
Processes and functions are given a unique identification number when they are created. You can use this to manipulate its execution (using signal), access local variables and its public variables (using the
. operator). We will discuss more on this later.
Now let's create another process:
import "mod_say" import "mod_time" import "mod_proc" // import a process manipulator module, for let_me_alone() Process Another() // start the definition of another process Begin // start the code Loop // endlessly say something say(get_timer() + " - Another"); frame; End End // end the definition of another process Process Main() Begin Another(); // start another process while(get_timer()<1000) say(get_timer() + " - Main"); frame; end let_me_alone(); // kill ALL processes EXCEPT this one End
Notice that Main and Another are executed one after the other. If we were to limit the FPS to 25, they both would run at 25FPS, one after the other.
Multiple source files
If you want to use multiple sources files - which you want for larger projects, you are able to accomplish this by using the include statement.
Consider the last example. We'll cut it up in a .prg and a .inc.
import "mod_say" import "mod_time" import "mod_proc" include "example.inc" // include example.inc: it will be as if the contents of // example.inc were located at this point in example.prg Process Main() Begin Another(); while(get_timer()<1000) say(get_timer() + " - Main"); frame; end let_me_alone(); End
import "mod_say" // we use these modules in this file, so it is good practise import "mod_time" // to (also) include them here Process Another() Begin Loop say(get_timer() + " - Another"); frame; End End
Variables and datatypes
There are multiple scopes of variables in Bennu:
- Global variables are variables available throughout the code (from the point of declaration)
- Constants are like global variables, but their value cannot be changed.
- Private variables are variables only available to the process or function in which they were declared.
- Public variables are like private variables, but they can also be accessed outside of the owning process.
- Local variables are like public variables, but they apply to all processes instead of a single one.
Bennu has various default types of variables:
|type||- can contain|
|int (32bits)|| - |
|dword (unsigned int)|| - |
|short (16bits)|| - |
|word (unsigned short)|| - |
|char (8bits)|| - |
|byte (unsigned char)|| - |
|float (32bits float)|| - |
To declare global variables, use Global:
Global int i; float f; End
To declare constants, use Const. Notice that there is not a datatype declared:
Const i = 0; f = 0.0; End
To declare private variables, use Private inside a process or function definition:
Process myprocess() Private int i; float f; End Begin End
To declare public variables, use Public inside a process or function definition:
Process myprocess() Public int i; float f; End Begin End
To declare local variables, use Local:
Local int i; float f; End
Arrays, Structs and pointers
Global // or somewhere else it is allowed to declare variables int my_array_of_ten_integers; // this makes an array of ten integers 0..9 End
We can access these integers like this:
my_int = my_array_of_ten_integers; my_index = 9; my_int = my_array_of_ten_integers[my_index];
Structs are formed using the keyword struct:
Global // or somewhere else it is allowed to declare variables Struct MyStruct // makes a struct with two members int i; float f; End End
We can access members by using the [[.]] operator:
my_int = MyStruct.i; my_float = MyStruct.f;
We can also make an array of structs:
Global // or somewhere else it is allowed to declare variables Struct MyStruct // makes ten structs int i; float f; End End
Pointers are variables which can 'point' to variables. They actually hold the address where that variable is located. If for some reason you move the variable (maybe because you used realloc()), the pointer no longer points to the variable. This is important to understand: the pointer points to a memory address. The operators * and & are used when dealing with pointers:
import "mod_say" Global int my_int; int* my_int_pointer; End Process Main() Begin my_int = 4; my_int_pointer = &my_int; // the & operator returns the address of the variable say("my_int: " + my_int); say("my_int_pointer: " + my_int_pointer); say("*my_int_pointer: " + *my_int_pointer); // the * operator dereferences the variable and returns the contents End
my_int: 4 my_int_pointer: 003FD134 *my_int_pointer: 4
So you see how you can use pointers. If you don't understand them yet or don't see their use, just forget about them for now, but remember they are there for when you need them.
Making your own types can be useful at times. In Bennu, your own types are structs, meaning they can hold multiple variables. You can read more about it here. A quick example:
Type Point float x; float y; End Global Point A,B; End Process Main() Begin A.x = 0; A.y = 1; B.x = A.x; B.y = 6; End
ProcessID, Public and Local
A process or function returns its ID when it reaches a frame-statement. We can assign this to a variable and use it later on.
We could rewrite the earlier example to this:
import "mod_say" import "mod_time" import "mod_proc" // import a process manipulator module, for signal() Process Another() Begin Loop say(get_timer() + " - Another"); frame; End End Process Main() Private Another a; int b; End Begin a = Another(); // start another process b = Another(); // start another process while(get_timer()<1000) say(get_timer() + " - Main"); frame; end signal(a,S_KILL); // kill a signal(b,S_KILL); // kill b End
You might wonder what the difference is between
Another a and
int b. The difference is that when you make a variable with the datatype the name of a process, Bennu can use this information to address public variables. Bennu does not know the type of process b is holding, so it can't access its public variables, because it can't know it has any. Locals however apply to all processes, so they can be accessed with both a and b.
import "mod_say" import "mod_time" import "mod_proc" Local int loc = 1; End Process Another() Public int pub = 2; Begin Loop frame; End End Process Main() Private Another a; int b; Begin a = Another(); // start another process b = Another(); // start another process say("a.loc = " + a.loc); say("b.loc = " + b.loc); say("a.pub = " + a.pub); //say("b.pub = " + b.pub); // not possible signal(a,S_KILL); // kill a signal(b,S_KILL); // kill b End
Arguments, Parameters and Return
You can pass processes and functions information using parameters. They can also return a value. When the process or function reaches a frame-statement, its processID is returned, which can be saved in a variable, or otherwise used. If it reaches a return statement first, then it will return the value supplied with the return statement. If
return; is used, or the process or function reaches neither a return-statement or a frame-statement, the ID is also returned. This sums up to: a process or function returns its ID, unless otherwise specified.
import "mod_say" Process Main() Begin say("multiply(3,4) = " + multiply(3,4)); End Function int multiply(int a, int b) Begin return a*b; End
import "mod_say" import "mod_proc" Process Main() Begin say("an object with ID: " + object()); say("an object with ID: " + object()); say("an object with ID: " + object()); let_me_alone(); End Process object() Begin Loop frame; End End
Sometimes, you want to know how a process is declared before it is defined. When you declare something, it is not yet defined, but if you define something, it is declared. It is good practice to only use things that are declared when used, otherwise you could get lost by tracing weird bugs you caused. But what if you want to call process A inside process B and the other way around? You would have to declare both processes beforehand, using Declare:
Declare Process A() End Declare Process B() End Process A() Begin B(); End Process B() Begin A(); End
You may have already noticed that this code is pretty bad: A will call B, B will call A, A will call B, etc.
You can use Declare for private variables and public variables too:
Declare Process A() Public int i; End End Declare Process B() Public float f; End End Process A() Private B b; Begin b = B(); b.f = 5; End Process B() Begin f = 1; say("f: " + f); frame; // let A continue say("f: " + f); End
Strings are a special kind of datatype. They could be described as a dynamic array of characters, meaning that the length of the characters can vary. Strings are mostly used for holding text.
Const MSG_ERROR = "Oops, error occured!"; MSG_SUCCESS = "Success!"; End Process Main() Private int error; // doesn't really do anything Begin if(error) say(MSG_ERROR); else say(MSG_SUCCESS); end End
Of course, you can do stuff with strings too. Let a and b be strings:
a + b = the concatenation of a and b ("ab"+"cd"="abcd"). More functionality for strings can be found in mod_string.
- basic statements
- angle,x,y,z,graph,pi etc with processes (make this article!)
- fpg,fgc: graphics libraries (make this article!)
- blendops (this one is somewhere)
- graphics article (make this article!)
- window manager
actually every module should have an article about it, telling what's it for