The Vale Programming Language

Extern Functions

Vale can call functions written in other languages, such as C. These "external" functions are known as extern functions.

Here, main is calling out into an external C function that reads an int from the keyboard.

The extern func readInt() int; is telling Vale that the C code will declare a function named readInt.

The C code must declare the function with extern, so it's visible to Vale.

If our vale code is in src/main.vale, and we run the program via valec build myproject=./src, then we can include the C code in the project by either:

  • Putting the C code inside src/native/; it will be automatically included.
  • Handing it in manually, like valec build myproject=./src myproject=/some/dir/myccode.c

vale
import stdlib.*;

exported func main() {
i = readInt();
println("User entered: " + i);

}

extern func readInt() int;
c
#include <stdint.h>
#include <stdio.h>
#include "mvtest/readInt.h"
extern ValeInt mvtest_readInt() {
  int64_t x = 0;
  scanf("%ld", &x);
  return x;
}
stdin
42
stdout
User entered: 42

In C, the module name is prepended to the function name. In the above example, extern func readInt() int in the mvtest module became mvtest_Vec3 in C.

Vale types have counterparts in C:

  • A Vale int is a C ValeInt, which is a typedef of int32_t. 0
  • A Vale i64 is a int64_t in C.
  • A Vale bool is a int8_t in C.
  • A Vale str is a ValeStr in C, with length and chars members.
    • The chars members is also null-terminated, for better C compatibility.
Side Notes
(interesting tangential thoughts)
0

Vale's int is 32 bits for compatibility and performance reasons.

Generated Headers

When defining extern functions, it is extremely recommended to include the header file generated by valec.

The above mvtest/readInt.h contains simply this:

c
extern ValeInt mvtest_readInt();

If you include this, the C compiler can detect a mismatch between the signature Vale expects and the signature you write.

So, if you write this function by accident:

c
extern int mvtest_readInt() {
  ...
}

The C compiler will detect your incorrect signature. This isn't possible if you forget to #include "mvtest/readInt.h".

If you're curious, these headers will be generated in the /include/ subdirectory, for example build/include/stdlib. The valec automatically passes -Ibuild/include to the final clang invocation so the .c files can see these generated headers.

Where do I put my C code?

The simplest way is to provide it as a standalone file for the module on the command line, such as the mymodule=src/myccode.c here:

valec build mymodule=src/main.vale mymodule=src/myccode.c --output_dir=build

A better option is to put it in your module's native subdirectory. valec automatically compiles any .c code it finds in a native/ subdirectory under your module.

For example, if we said valec build mymodule=src/main.vale --output_dir=build and we had a file src/native/myccode.c, valec would compile it in automatically.

Export Functions

The above Vale code called into a C function. The opposite is also possible, C code can call into Vale code. These Vale functions are known as export functions.

Here, a Vale main is calling into a C cFunc function, which is calling into a Vale exported triple function.

Vale automatically provides a header file so C can call the function.

vale
extern func cFunc();
exported func main() { cFunc(); }

exported func triple(x int) int {
x * 3
}
c
#include <stdint.h>
#include <stdio.h>
#include "mvtest/cFunc.h"
#include "mvtest/triple.h"
extern void mvtest_cFunc() {
  printf("%ld", mvtest_triple(14));
}
stdout
42

Structs

Recall that a struct can either be an mutable or immutable. The default is mut. These offer different benefits when calling into (or from) outside code.

To make a struct usable from C, add the exported keyword to its definition (the next section has some examples). A header is generated for it, similar to a function's header.

Every member of an exported immutable struct must be of an exported type.

Immutable and mutable structs behave differently when exported, read on to learn how.

Immutable Structs

When we send an immutable struct from Vale into C, we're actually sending a copy. 1

Here, a Vale main function is sending an immutable Vec3 struct into C.

The C code should include "Vec3.h", which is generated by valec.

vale
import stdlib.*;

exported struct Vec3 imm {
x int; y int; z int;
}
exported func main() {
v = Vec3(10, 11, 12);
s = sum(v);
println(s);

}

extern func sum(v Vec3) int;
c
#include <stdint.h>
#include "mvtest/Vec3.h"
#include "mvtest/sum.h"
extern int mvtest_sum(mvtest_Vec3* v) {
  int result = v->x + v->y + v->z;
  free(v);
  return result;
}
stdout
33

The C code must remember to free() this copy.

1

Instance structs do not incur this cost. More on that below!

Mutable Structs

We can give C a mutable struct too.

C will receive an "opaque handle", something that points to the struct, but C cannot read its members. It must ask a Vale function to read any members.

The benefit of using mutable structs in externs is that Vale doesn't have to copy the entire thing when we give it to C.

Here, a Vale main function is giving C a reference to a Ship.

The C function halfFuel is then asking the Vale function getFuel what the contained fuel is, so C can do its calculation, and return the result.

vale
import stdlib.*;

exported struct Ship {
fuel int;
}
exported func getFuel(s &Ship) int {
s.fuel
}

exported func main() {
s = Ship(42);
h = halfFuel(&s);
println(h);

}

extern func halfFuel(s &Ship) int;
c
#include <stdint.h>
#include "mvtest/Ship.h"
#include "mvtest/halfFuel.h"
#include "mvtest/getFuel.h"
extern int mvtest_halfFuel(mvtest_ShipRef s) {
  return mvtest_getFuel(s) / 2;
}
stdout
21

Arrays

To use a Vale array in C, we must give it a name, using the export statement, like in this example.

Note that #[]int is the syntax for an immutable runtime-sized array.

vale
export #[]int as ImmIntArray;

extern func sumBytes(arr #[]int) int;

exported func main() int {
a = #[](5, x => x);
return sumBytes(a);

}
c
#include <stdint.h>
#include <stdio.h>
#include "vtest/ImmIntArray.h"

ValeInt vtest_sumBytes(vtest_ImmIntArray* arr) {
  ValeInt total = 0;
  for (int i = 0; i < arr->length; i++) {
    total += arr->elements[i];
  }
  free(arr);
  return total;
}

Fearless FFI

Vale is the most memory-safe native language: bugs in C cannot affect Vale objects. 2 This is because:

  • When we send a mutable struct's reference into C, it will be scrambled 3 4 so that C cannot reach through it into the object's memory.
  • When we send an immutable struct into C, it's copied, so C cannot reach the original struct's memory.

This design also enables perfect replayability, a coming feature that allows us to record an execution's inputs (from the user's keyboard, extern functions' returns, export functions' parameters) which enables us to replay it exactly. 5

2

One cannot accidentally corrupt Vale objects. This however does not prevent someone from intentionally working around the protections here. Don't run untrusted C code in your programs.

3

It's rotated and xor'd by a random integer determined at compile-time, which is enough to prevent accidental accesses.

4

This scrambling is unimplemented, stay tuned!

5

When multi-threading comes, we hope to make it record the order in which we lock mutexes and receive messages, to unerringly replay race conditions. Perhaps heisenbugs will be a thing of the past!

Next: Unsafe