Scripting environment
The Berry scripts run on a virtual machine that can communicate with native code via a virtual stack. The virtual stack only accepts specific data types. Therefore, any data transferred from the native code to the virtual machine, or vice versa, must be converted.
A user script is interpreted a single time. Upon widget events, the system invokes function the callbacks declared in the user script. The script interacts with the widget by calling methods that invoke a corresponding function written in native code - also known as native functions. Native function are registered by the system before interpreting any user scripts.
Native functions must be able to query and modify the state of the widgets.
To do this, the native function must know which widget is being queried or modified and have access to the widgets’ state pointer.
The native function is executed as a method on an object.
An object associated to a given widget contains the index of this widget that allows identifying which widget is being queried or modified.
The pointer to the widgets’ state is passed through a global variable named _SMOLPHONE_CONTEXT_.
We assume that this variable is never used or modified by the user script.
Modifying the value of this variable can lead to critical failures, such as segmentation faults.
See the following section for more details and possible alternative to avoid this issue.
Pointer passing issues
To query and modify widgets’ state, the native code pass a pointer to the user scripts that in turn pass it to native functions. From a native code perspective, we pass a pointer to a black box, and we retrieve a pointer from this black box. The native code and this black box communicate using Foreign Function Invocations.
Passing and retrieving pointers around FFI boundaries is a pretty bad idea from a Rust perspective.
Indeed, Rust has some assumptions about pointers that allow Rust to do many optimizations or simply to ensure memory safety.
For example, safe Rust ensures that a mutable reference cannot be aliased and that a pointer is not used beyond its lifespan.
This allows Rust to freely move data to different memory locations when it has a mutable access to a data.
When Rust passes a pointer to the Virtual Machine it is only mutably borrowed for the time of the invocation.
The Virtual machine has no concept of data borrowing and mutability.
The VM can hold the reference infinitely.
If Rust moves the data in another memory location or deallocates it, this results in a dangling pointer and a possible segmentation fault on pointer dereferencing.
On the Rust side, the pointer can be pinned to avoid any move.
To avoid deallocation, we could use a pointer with a static lifetime.
However, this creates too restrictive constraints that make it impractical.
This is why we gave up by introducing a wrapper type Borrowed that basically makes the promise that the pointer that we pass to the Virtual Machine will not be held.
Moreover, a user script can pass any pointer to native functions. This can lead to segmentation faults or undefined behaviors. We assume that the script always pass the expected pointer.
Thus, passing pointers to the Virtual machine is a pretty bad idea. This creates many challenges that are hard and even impossible to solve. Ideally, the pointer should be directly passed to native functions as a context that cannot be accessed and modified by the user scripts. Berry and similar languages don’t afford this feature.
Error handling
Berry scripts have no standard mean of handling errors. Any reported errors cause the end of the execution of the script.