74 KiB
Advanced Topics
Attributes
V has several attributes that modify the behavior of functions and structs.
An attribute is a compiler instruction specified inside [] right before a
function/struct/enum declaration and applies only to the following declaration.
// @[flag] enables Enum types to be used as bitfields
@[flag]
enum BitField {
read
write
other
}
fn main() {
assert 1 == int(BitField.read)
assert 2 == int(BitField.write)
mut bf := BitField.read
assert bf.has(.read | .other) // test if *at least one* of the flags is set
assert !bf.all(.read | .other) // test if *all* of the flags are set
bf.set(.write | .other)
assert bf.has(.read | .write | .other)
assert bf.all(.read | .write | .other)
bf.toggle(.other)
assert bf == BitField.read | .write
assert bf.all(.read | .write)
assert !bf.has(.other)
empty := BitField.zero()
assert empty.is_empty()
assert !empty.has(.read)
assert !empty.has(.write)
assert !empty.has(.other)
mut full := empty
full.set_all()
assert int(full) == 7 // 0x01 + 0x02 + 0x04
assert full == .read | .write | .other
mut v := full
v.clear(.read | .other)
assert v == .write
v.clear_all()
assert v == empty
assert BitField.read == BitField.from('read')!
assert BitField.other == BitField.from('other')!
assert BitField.write == BitField.from(2)!
assert BitField.zero() == BitField.from('')!
}
Struct field deprecations:
module abc
// Note that only *direct* accesses to Xyz.d in *other modules*, will produce deprecation notices/warnings:
pub struct Xyz {
pub mut:
a int
d int @[deprecated: 'use Xyz.a instead'; deprecated_after: '2999-03-01']
// the tags above, will produce a notice, since the deprecation date is in the far future
}
Function/method deprecations:
// Calling this function will result in a deprecation warning
@[deprecated]
fn old_function() {
}
// It can also display a custom deprecation message
@[deprecated: 'use new_function() instead']
fn legacy_function() {}
// You can also specify a date, after which the function will be
// considered deprecated. Before that date, calls to the function
// will be compiler notices - you will see them, but the compilation
// is not affected. After that date, calls will become warnings,
// so ordinary compiling will still work, but compiling with -prod
// will not (all warnings are treated like errors with -prod).
// 6 months after the deprecation date, calls will be hard
// compiler errors.
@[deprecated: 'use new_function2() instead']
@[deprecated_after: '2021-05-27']
fn legacy_function2() {}
// This function's calls will be inlined.
@[inline]
fn inlined_function() {
}
// This function's calls will NOT be inlined.
@[noinline]
fn function() {
}
// This function will NOT return to its callers.
// Such functions can be used at the end of or blocks,
// just like exit/1 or panic/1. Such functions can not
// have return types, and should end either in for{}, or
// by calling other `[noreturn]` functions.
@[noreturn]
fn forever() {
for {}
}
// The following struct must be allocated on the heap. Therefore, it can only be used as a
// reference (`&Window`) or inside another reference (`&OuterStruct{ Window{...} }`).
// See section "Stack and Heap"
@[heap]
struct Window {
}
// Calls to following function must be in unsafe{} blocks.
// Note that the code in the body of `risky_business()` will still be
// checked, unless you also wrap it in `unsafe {}` blocks.
// This is useful, when you want to have an `[unsafe]` function that
// has checks before/after a certain unsafe operation, that will still
// benefit from V's safety features.
@[unsafe]
fn risky_business() {
// code that will be checked, perhaps checking pre conditions
unsafe {
// code that *will not be* checked, like pointer arithmetic,
// accessing union fields, calling other `[unsafe]` fns, etc...
// Usually, it is a good idea to try minimizing code wrapped
// in unsafe{} as much as possible.
// See also [Memory-unsafe code](#memory-unsafe-code)
}
// code that will be checked, perhaps checking post conditions and/or
// keeping invariants
}
// V's autofree engine will not take care of memory management in this function.
// You will have the responsibility to free memory manually yourself in it.
// Note: it is NOT related to the garbage collector. It will only make the
// -autofree mechanism, ignore the body of that function.
@[manualfree]
fn custom_allocations() {
}
// The memory pointed to by the pointer arguments of this function will not be
// freed by the garbage collector (if in use) before the function returns
// For C interop only.
@[keep_args_alive]
fn C.my_external_function(voidptr, int, voidptr) int
// A @[weak] tag tells the C compiler, that the next declaration will be weak, i.e. when linking,
// if there is another declaration of a symbol with the same name (a 'strong' one), it should be
// used instead, *without linker errors about duplicate symbols*.
// For C interop only.
@[weak]
__global abc = u64(1)
// Tell V, that the following global was defined on the C side,
// thus V will not initialise it, but will just give you access to it.
// For C interop only.
@[c_extern]
__global my_instance C.my_struct
struct C.my_struct {
a int
b f64
}
// Tell V that the following struct is defined with `typedef struct` in C.
// For C interop only.
@[typedef]
pub struct C.Foo {}
// Used to add a custom calling convention to a function, available calling convention: stdcall, fastcall and cdecl.
// This list also applies for type aliases (see below).
// For C interop only.
@[callconv: 'stdcall']
fn C.DefWindowProc(hwnd int, msg int, lparam int, wparam int)
// Used to add a custom calling convention to a function type aliases.
// For C interop only.
@[callconv: 'fastcall']
type FastFn = fn (int) bool
// Windows only:
// Without this attribute all graphical apps will have the following behavior on Windows:
// If run from a console or terminal; keep the terminal open so all (e)println statements can be viewed.
// If run from e.g. Explorer, by double-click; app is opened, but no terminal is opened, and no
// (e)println output can be seen.
// Use it to force-open a terminal to view output in, even if the app is started from Explorer.
// Valid before main() only.
@[console]
fn main() {
}
Conditional compilation
The goal of this feature, is to tell V to not compile a function, and all its calls, in the final executable, if a provided custom flag is not passed.
V will still type check the function and all its calls, even if they will not be present in the final executable, due to the passed -d flags.
In order to see it in action, run the following example with v run example.v once,
and then a second time with v -d trace_logs example.v:
@[if trace_logs ?]
fn elog(s string) {
eprintln(s)
}
fn main() {
elog('some expression: ${2 + 2}') // such calls will not be done *at all*, if `-d trace_logs` is not passed
println('hi')
elog('finish')
}
Conditional compilation, based on custom flags, can also be used to produce slightly different executables, which share the majority of the same code, but where some of the logic, is needed only some of the time, for example a network server/client program can be written like so:
fn act_as_client() { ... }
fn act_as_server() { ... }
fn main() {
$if as_client ? {
act_as_client()
}
$if as_server ? {
act_as_server()
}
}
To generate a client.exe executable do: v -d as_client -o client.exe .
To generate a server.exe executable do: v -d as_server -o server.exe .
Compile time pseudo variables
V also gives your code access to a set of pseudo string variables, that are substituted at compile time:
@FN=> replaced with the name of the current V function.@METHOD=> replaced with ReceiverType.MethodName.@MOD=> replaced with the name of the current V module.@STRUCT=> replaced with the name of the current V struct.@FILE=> replaced with the absolute path of the V source file.@LINE=> replaced with the V line number where it appears (as a string).@FILE_LINE=> like@FILE:@LINE, but the file part is a relative path.@LOCATION=> file, line and name of the current type + method; suitable for logging.@COLUMN=> replaced with the column where it appears (as a string).@VEXE=> replaced with the path to the V compiler.@VEXEROOT=> will be substituted with the folder, where the V executable is (as a string).@VHASH=> replaced with the shortened commit hash of the V compiler (as a string).@VCURRENTHASH=> Similar to@VHASH, but changes when the compiler is recompiled on a different commit (after local modifications, or after using git bisect etc).@VMOD_FILE=> replaced with the contents of the nearest v.mod file (as a string).@VMODHASH=> is replaced by the shortened commit hash, derived from the .git directory next to the nearest v.mod file (as a string).@VMODROOT=> will be substituted with the folder, where the nearest v.mod file is (as a string).
That allows you to do the following example, useful while debugging/logging/tracing your code:
eprintln(@LOCATION)
Another example, is if you want to embed the version/name from v.mod inside your executable:
import v.vmod
vm := vmod.decode( @VMOD_FILE ) or { panic(err) }
eprintln('${vm.name} ${vm.version}\n ${vm.description}')
A program that prints its own source code (a quine):
print($embed_file(@FILE).to_string())
Note
you can have arbitrary source code in the file, without problems, since the full file will be embedded into the executable, produced by compiling it. Also note that printing is done with
println, to not add another new line, missing in the source code.
Compile time reflection
$ is used as a prefix for compile time (also referred to as 'comptime') operations.
Having built-in JSON support is nice, but V also allows you to create efficient
serializers for any data format. V has compile time if and for constructs:
.fields
You can iterate over struct fields using .fields, it also works with generic types
(e.g. T.fields) and generic arguments (e.g. param.fields where fn gen[T](param T) {).
struct User {
name string
age int
}
fn main() {
$for field in User.fields {
$if field.typ is string {
println('${field.name} is of type string')
}
}
}
// Output:
// name is of type string
.values
You can read Enum values and their attributes.
enum Color {
red @[RED] // first attribute
blue @[BLUE] // second attribute
}
fn main() {
$for e in Color.values {
println(e.name)
println(e.attrs)
}
}
// Output:
// red
// ['RED']
// blue
// ['BLUE']
.attributes
You can read Struct attributes.
@[COLOR]
struct Foo {
a int
}
fn main() {
$for e in Foo.attributes {
println(e)
}
}
// Output:
// StructAttribute{
// name: 'COLOR'
// has_arg: false
// arg: ''
// kind: plain
// }
.variants
You can read variant types from Sum type.
type MySum = int | string
fn main() {
$for v in MySum.variants {
$if v.typ is int {
println('has int type')
} $else $if v.typ is string {
println('has string type')
}
}
}
// Output:
// has int type
// has string type
.methods
You can retrieve information about struct methods.
struct Foo {
}
fn (f Foo) test() int {
return 123
}
fn (f Foo) test2() string {
return 'foo'
}
fn main() {
foo := Foo{}
$for m in Foo.methods {
$if m.return_type is int {
print('${m.name} returns int: ')
println(foo.$method())
} $else $if m.return_type is string {
print('${m.name} returns string: ')
println(foo.$method())
}
}
}
// Output:
// test returns int: 123
// test2 returns string: foo
See examples/compiletime/reflection.v
for a more complete example.
Compile time code
$if condition
fn main() {
// Support for multiple conditions in one branch
$if ios || android {
println('Running on a mobile device!')
}
$if linux && x64 {
println('64-bit Linux.')
}
// Usage as expression
os := $if windows { 'Windows' } $else { 'UNIX' }
println('Using ${os}')
// $else-$if branches
$if tinyc {
println('tinyc')
} $else $if clang {
println('clang')
} $else $if gcc {
println('gcc')
} $else {
println('different compiler')
}
$if test {
println('testing')
}
// v -cg ...
$if debug {
println('debugging')
}
// v -prod ...
$if prod {
println('production build')
}
// v -d option ...
$if option ? {
println('custom option')
}
}
If you want an if to be evaluated at compile time it must be prefixed with a $ sign.
Right now it can be used to detect an OS, compiler, platform or compilation options.
$if debug is a special option like $if windows or $if x32, it's enabled if the program
is compiled with v -g or v -cg.
If you're using a custom ifdef, then you do need $if option ? {} and compile withv -d option.
Full list of builtin options:
| OS | Compilers | Platforms | Other |
|---|---|---|---|
windows, linux, macos |
gcc, tinyc |
amd64, arm64, aarch64 |
debug, prod, test |
darwin, ios, bsd |
clang, mingw |
i386, arm32 |
js, glibc, prealloc |
freebsd, openbsd, netbsd |
msvc |
rv64, rv32 |
no_bounds_checking, freestanding |
android, mach, dragonfly |
cplusplus |
x64, x32 |
no_segfault_handler, no_backtrace |
gnu, hpux, haiku, qnx |
little_endian, big_endian |
no_main, fast_math, apk, threads |
|
solaris, termux |
js_node, js_browser, js_freestanding |
||
serenity, vinix, plan9 |
interpreter, es5, profile, wasm32 |
||
wasm32_emscripten, wasm32_wasi |
|||
native, autofree |
$embed_file
import os
fn main() {
embedded_file := $embed_file('v.png')
os.write_file('exported.png', embedded_file.to_string())!
}
V can embed arbitrary files into the executable with the $embed_file(<path>)
compile time call. Paths can be absolute or relative to the source file.
Note that by default, using $embed_file(file), will always embed the whole content
of the file, but you can modify that behaviour by passing: -d embed_only_metadata
when compiling your program. In that case, the file will not be embedded. Instead,
it will be loaded the first time your program calls embedded_file.data() at runtime,
making it easier to change in external editor programs, without needing to recompile
your program.
Embedding a file inside your executable, will increase its size, but
it will make it more self contained and thus easier to distribute.
When that happens (the default), embedded_file.data() will cause no IO,
and it will always return the same data.
$embed_file supports compression of the embedded file when compiling with -prod.
Currently only one compression type is supported: zlib.
import os
fn main() {
embedded_file := $embed_file('x.css', .zlib) // compressed using zlib
os.write_file('exported.css', embedded_file.to_string())!
}
Note: compressing binary assets like png or zip files, usually will not gain you much, and in some cases may even take more space in the final executable, since they are already compressed.
$embed_file returns
EmbedFileData
which could be used to obtain the file contents as string or []u8.
$tmpl for embedding and parsing V template files
V has a simple template language for text and html templates, and they can easily
be embedded via $tmpl('path/to/template.txt'):
fn build() string {
name := 'Peter'
age := 25
numbers := [1, 2, 3]
return $tmpl('1.txt')
}
fn main() {
println(build())
}
1.txt:
name: @name
age: @age
numbers: @numbers
@for number in numbers
@number
@end
output:
name: Peter
age: 25
numbers: [1, 2, 3]
1
2
3
See more details
$env
module main
fn main() {
compile_time_env := $env('ENV_VAR')
println(compile_time_env)
}
V can bring in values at compile time from environment variables.
$env('ENV_VAR') can also be used in top-level #flag and #include statements:
#flag linux -I $env('JAVA_HOME')/include.
$d
V can bring in values at compile time from -d ident=value flag defines, passed on
the command line to the compiler. You can also pass -d ident, which will have the
same meaning as passing -d ident=true.
To get the value in your code, use: $d('ident', default), where default
can be false for booleans, 0 or 123 for i64 numbers, 0.0 or 113.0
for f64 numbers, 'a string' for strings.
When a flag is not provided via the command line, $d() will return the default
value provided as the second argument.
module main
const my_i64 = $d('my_i64', 1024)
fn main() {
compile_time_value := $d('my_string', 'V')
println(compile_time_value)
println(my_i64)
}
Running the above with v run . will output:
V
1024
Running the above with v -d my_i64=4096 -d my_string="V rocks" run . will output:
V rocks
4096
Here is an example of how to use the default values, which have to be pure literals:
fn main() {
val_str := $d('id_str', 'value') // can be changed by providing `-d id_str="my id"`
val_f64 := $d('id_f64', 42.0) // can be changed by providing `-d id_f64=84.0`
val_i64 := $d('id_i64', 56) // can be changed by providing `-d id_i64=123`
val_bool := $d('id_bool', false) // can be changed by providing `-d id_bool=true`
val_char := $d('id_char', `f`) // can be changed by providing `-d id_char=v`
println(val_str)
println(val_f64)
println(val_i64)
println(val_bool)
println(rune(val_char))
}
$d('ident','value') can also be used in top-level statements like #flag and #include:
#flag linux -I $d('my_include','/usr')/include. The default value for $d when used in these
statements should be literal strings.
$d('ident', false) can also be used inside $if $d('ident', false) { statements,
granting you the ability to selectively turn on/off certain sections of code, at compile
time, without modifying your source code, or keeping different versions of it.
$compile_error and $compile_warn
These two comptime functions are very useful for displaying custom errors/warnings during compile time.
Both receive as their only argument a string literal that contains the message to display:
// x.v
module main
$if linux {
$compile_error('Linux is not supported')
}
fn main() {
}
$ v run x.v
x.v:4:5: error: Linux is not supported
2 |
3 | $if linux {
4 | $compile_error('Linux is not supported')
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 | }
6 |
Compile time types
Compile time types group multiple types into a general higher-level type. This is useful in
functions with generic parameters, where the input type must have a specific property, for example
the .len attribute in arrays.
V supports the following compile time types:
$alias=> matches Type aliases.$array=> matches Arrays and Fixed Size Arrays.$array_dynamic=> matches Arrays, but not Fixed Size Arrays.$array_fixed=> matches Fixed Size Arrays, but not Arrays$enum=> matches Enums.$float=> matchesf32,f64and float literals.$function=> matches Function Types.$int=> matchesint,i8,i16,i32,i64,u8,u16,u32,u64,isize,usizeand integer literals.$interface=> matches Interfaces.$map=> matches Maps.$option=> matches Option Types.$struct=> matches Structs.$sumtype=> matches Sum Types.$string=> matches Strings.
Environment specific files
If a file has an environment-specific suffix, it will only be compiled for that environment.
.js.v=> will be used only by the JS backend. These files can contain JS. code..c.v=> will be used only by the C backend. These files can contain C. code..native.v=> will be used only by V's native backend._nix.c.v=> will be used only on Unix systems (non Windows)._${os}.c.v=> will be used only on the specificossystem. For example,_windows.c.vwill be used only when compiling on Windows, or with-os windows._default.c.v=> will be used only if there is NOT a more specific platform file. For example, if you have bothfile_linux.c.vandfile_default.c.v, and you are compiling for linux, then onlyfile_linux.c.vwill be used, andfile_default.c.vwill be ignored.
Here is a more complete example: main.v:
module main
fn main() { println(message) }
main_default.c.v:
module main
const message = 'Hello world'
main_linux.c.v:
module main
const message = 'Hello linux'
main_windows.c.v:
module main
const message = 'Hello windows'
With the example above:
-
when you compile for windows, you will get 'Hello windows'
-
when you compile for linux, you will get 'Hello linux'
-
when you compile for any other platform, you will get the non specific 'Hello world' message.
-
_d_customflag.v=> will be used only if you pass-d customflagto V. That corresponds to$if customflag ? {}, but for a whole file, not just a single block.customflagshould be a snake_case identifier, it can not contain arbitrary characters (only lower case latin letters + numbers +_).Note
A combinatorial
_d_customflag_linux.c.vpostfix will not work. If you do need a custom flag file, that has platform dependent code, use the postfix_d_customflag.v, and then use platform dependent compile time conditional blocks inside it, i.e.$if linux {}etc. -
_notd_customflag.v=> similar to _d_customflag.v, but will be used only if you do NOT pass-d customflagto V.
See also Cross Compilation.
Debugger
To use the native V debugger, add the $dbg statement to your source, where you
want the debugger to be invoked.
fn main() {
a := 1
$dbg;
}
Running this V code, you will get the debugger REPL break when the execution
reaches the $dbg statement.
$ v run example.v
Break on [main] main in example.v:3
example.v:3 vdbg>
At this point, execution is halted, and the debugger is now available.
To see the available commands, type ?, h or help. (Completion for commands works - Non-Windows only)
example.v:3 vdbg> ?
vdbg commands:
anon? check if the current context is anon
bt prints a backtrace
c, continue continue debugging
generic? check if the current context is generic
heap show heap memory usage
h, help, ? show this help
l, list [lines] show some lines from current break (default: 3)
mem, memory show memory usage
method? check if the current context is a method
m, mod show current module name
p, print <var> prints an variable
q, quit exits debugging session in the code
scope show the vars in the current scope
u, unwatch <var> unwatches a variable
w, watch <var> watches a variable
Lets try the scope command, to inspect the current scope context.
example.v:3 vdbg> scope
a = 1 (int)
Cool! We have the variable name, its value and its type name.
What about printing only a variable, not the whole scope?
Just type p a.
To watch a variable by its name, use:
w a (where a is the variable name)
To stop watching the variable (unwatch it), use u a.
Lets see more one example:
fn main() {
for i := 0; i < 4; i++ {
$dbg
}
}
Running again, we'll get:
Break on [main] main in example.v:3
If we want to read the source code context, we can use the l or list command.
example.v:3 vdbg> l
0001 fn main() {
0002 for i := 0; i < 4; i++ {
0003> $dbg
0004 }
0005 }
The default is read 3 lines before and 3 lines after, but you can
pass a parameter to the command to read more lines, like l 5.
Now, lets watch the variable changing on this loop.
example.v:3 vdbg> w i
i = 0 (int)
To continue to the next breakpoint, type c or continue command.
example.v:3 vdbg> c
Break on [main] main in example.v:3
i = 1 (int)
i and it's value is automatically printed, because it is in the watch list.
To repeat the last command issued, in this case the c command,
just hit the enter key.
example.v:3 vdbg>
Break on [main] main in example.v:3
i = 2 (int)
example.v:3 vdbg>
Break on [main] main in example.v:3
i = 3 (int)
example.v:3 vdbg>
You can also see memory usage with mem or memory command, and
check if the current context is an anon function (anon?), a method (method?)
or a generic method (generic?) and clear the terminal window (clear).
Call stack
You can also show the current call stack with v.debug.
To enable this feature, add the -d callstack switch when building or running
your code:
import v.debug
fn test(i int) {
if i > 9 {
debug.dump_callstack()
}
}
fn do_something() {
for i := 0; i <= 10; i++ {
test(i)
}
}
fn main() {
do_something()
}
$ v -d callstack run example.v
Backtrace:
--------------------------------------------------
example.v:16 | > main.main
example.v:11 | > main.do_something
example.v:5 | > main.test
--------------------------------------------------
Trace
Another feature of v.debug is the possibility to add hook functions
before and after each function call.
To enable this feature, add the -d trace switch when building or running
your code:
import v.debug
fn main() {
hook1 := debug.add_before_call(fn (fn_name string) {
println('> before ${fn_name}')
})
hook2 := debug.add_after_call(fn (fn_name string) {
println('> after ${fn_name}')
})
anon := fn () {
println('call')
}
anon()
// optionally you can remove the hooks:
debug.remove_before_call(hook1)
debug.remove_after_call(hook2)
anon()
}
$ v -d trace run example.v
> before anon
call
> after anon
call
Memory-unsafe code
Sometimes for efficiency you may want to write low-level code that can potentially corrupt memory or be vulnerable to security exploits. V supports writing such code, but not by default.
V requires that any potentially memory-unsafe operations are marked intentionally. Marking them also indicates to anyone reading the code that there could be memory-safety violations if there was a mistake.
Examples of potentially memory-unsafe operations are:
- Pointer arithmetic
- Pointer indexing
- Conversion to pointer from an incompatible type
- Calling certain C functions, e.g.
free,strlenandstrncmp.
To mark potentially memory-unsafe operations, enclose them in an unsafe block:
// allocate 2 uninitialized bytes & return a reference to them
mut p := unsafe { malloc(2) }
p[0] = `h` // Error: pointer indexing is only allowed in `unsafe` blocks
unsafe {
p[0] = `h` // OK
p[1] = `i`
}
p++ // Error: pointer arithmetic is only allowed in `unsafe` blocks
unsafe {
p++ // OK
}
assert *p == `i`
Best practice is to avoid putting memory-safe expressions inside an unsafe block,
so that the reason for using unsafe is as clear as possible. Generally any code
you think is memory-safe should not be inside an unsafe block, so the compiler
can verify it.
If you suspect your program does violate memory-safety, you have a head start on
finding the cause: look at the unsafe blocks (and how they interact with
surrounding code).
Note
This is work in progress.
Structs with reference fields
Structs with references require explicitly setting the initial value to a reference value unless the struct already defines its own initial value.
Zero-value references, or nil pointers, will NOT be supported in the future,
for now data structures such as Linked Lists or Binary Trees that rely on reference
fields that can use the value 0, understanding that it is unsafe, and that it can
cause a panic.
struct Node {
a &Node
b &Node = unsafe { nil } // Auto-initialized to nil, use with caution!
}
// Reference fields must be initialized unless an initial value is declared.
// Zero (0) is OK but use with caution, it's a nil pointer.
foo := Node{
a: 0
}
bar := Node{
a: &foo
}
baz := Node{
a: 0
b: 0
}
qux := Node{
a: &foo
b: &bar
}
println(baz)
println(qux)
sizeof and __offsetof
sizeof(Type)gives the size of a type in bytes.__offsetof(Struct, field_name)gives the offset in bytes of a struct field.
struct Foo {
a int
b int
}
assert sizeof(Foo) == 8
assert __offsetof(Foo, a) == 0
assert __offsetof(Foo, b) == 4
Limited operator overloading
Operator overloading defines the behavior of certain binary operators for certain types.
struct Vec {
x int
y int
}
fn (a Vec) str() string {
return '{${a.x}, ${a.y}}'
}
fn (a Vec) + (b Vec) Vec {
return Vec{a.x + b.x, a.y + b.y}
}
fn (a Vec) - (b Vec) Vec {
return Vec{a.x - b.x, a.y - b.y}
}
fn main() {
a := Vec{2, 3}
b := Vec{4, 5}
mut c := Vec{1, 2}
println(a + b) // "{6, 8}"
println(a - b) // "{-2, -2}"
c += a
//^^ autogenerated from + overload
println(c) // "{3, 5}"
}
Operator overloading goes against V's philosophy of simplicity and predictability. But since scientific and graphical applications are among V's domains, operator overloading is an important feature to have in order to improve readability:
a.add(b).add(c.mul(d))is a lot less readable thana + b + c * d.
Operator overloading is possible for the following binary operators: +, -, *, /, %, <, ==.
Implicitly generated overloads
-
==is automatically generated by the compiler, but can be overridden. -
!=,>,<=and>=are automatically generated when==and<are defined. They cannot be explicitly overridden. -
Assignment operators (
*=,+=,/=, etc) are automatically generated when the corresponding operators are defined and the operands are of the same type. They cannot be explicitly overridden.
Restriction
To improve safety and maintainability, operator overloading is limited.
Type restrictions
- When overriding
<and==, the return type must be strictlybool. - Both arguments must have the same type (just like with all operators in V).
- Overloaded operators have to return the same type as the argument
(the exceptions are
<and==).
Other restrictions
- Arguments cannot be changed inside overloads.
- Calling other functions inside operator functions is not allowed (planned).
Performance tuning
When compiled with -prod, V's generated C code usually performs well. However, in specialized
scenarios, additional compiler flags and attributes can further optimize the executable for
performance, memory usage, or size.
Note
These are rarely needed, and should not be used unless you profile your code, and then see that there are significant benefits for them. To cite GCC's documentation: "Programmers are notoriously bad at predicting how their programs actually perform".
| Tuning Operation | Benefits | Drawbacks |
|---|---|---|
@[inline] |
Performance | Increased executable size |
@[direct_array_access] |
Performance | Safety risks |
@[packed] |
Memory usage | Potential performance loss |
@[minify] |
Performance, Memory usage | May break binary serialization/reflection |
_likely_/_unlikely_ |
Performance | Risk of negative performance impact |
-skip-unused |
Performance, Compile time, Size | Potential instability |
-fast-math |
Performance | Risk of incorrect mathematical operations results |
-d no_segfault_handler |
Compile time, Size | Loss of segfault trace |
-cflags -march=native |
Performance | Risk of reduced CPU compatibility |
-compress |
Size | Harder to debug, extra dependency upx |
PGO |
Performance, Size | Usage complexity |
Tuning operations details
@[inline]
You can tag functions with @[inline], so the C compiler will try to inline them, which in some
cases, may be beneficial for performance, but may impact the size of your executable.
When to Use
- Functions that are called frequently in performance-critical loops.
When to Avoid
- Large functions, as it might cause code bloat and actually decrease performance.
- Large functions in
ifexpressions - may have negative impact on instructions cache.
@[direct_array_access]
In functions tagged with @[direct_array_access] the compiler will translate array operations
directly into C array operations - omitting bounds checking. This may save a lot of time in a
function that iterates over an array but at the cost of making the function unsafe - unless the
boundaries will be checked by the user.
When to Use
- In tight loops that access array elements, where bounds have been manually verified or you are sure that the access index will be valid.
When to Avoid
- Everywhere else.
@[packed]
The @[packed] attribute can be applied to a structure to create an unaligned memory layout,
which decreases the overall memory footprint of the structure. Using the @[packed] attribute
may negatively impact performance or even be prohibited on certain CPU architectures.
When to Use
- When memory usage is more critical than performance, e.g., in embedded systems.
When to Avoid
- On CPU architectures that do not support unaligned memory access or when high-speed memory access is needed.
@[aligned]
The @[aligned] attribute can be applied to a structure or union to specify a minimum alignment
(in bytes) for variables of that type. Using the @[aligned] attribute you can only increase
the default alignment. Use @[packed] if you want to decrease it. The alignment of any struct
or union, should be at least a perfect multiple of the lowest common multiple of the alignments of
all of the members of the struct or union.
Example:
// Each u16 in the `data` field below, takes 2 bytes, and we have 3 of them = 6 bytes.
// The smallest power of 2, bigger than 6 is 8, i.e. with `@[aligned]`, the alignment
// for the entire struct U16s, will be 8:
@[aligned]
struct U16s {
data [3]u16
}
When to Use
- Only if the instances of your types, will be used in performance critical sections, or with specialised machine instructions, that do require a specific alignment to work.
When to Avoid
- On CPU architectures, that do not support unaligned memory access. If you are not working on performance critical algorithms, you do not really need it, since the proper minimum alignment is CPU specific, and the compiler already usually will choose a good default for you.
Note
You can leave out the alignment factor, i.e. use just
@[aligned], in which case the compiler will align a type to the maximum useful alignment for the target machine you are compiling for, i.e. the alignment will be the largest alignment which is ever used for any data type on the target machine. Doing this can often make copy operations more efficient, because the compiler can choose whatever instructions copy the biggest chunks of memory, when performing copies to or from the variables which have types that you have aligned this way.
See also "What Every Programmer Should Know About Memory", by Ulrich Drepper .
@[minify]
The @[minify] attribute can be added to a struct, allowing the compiler to reorder the fields in
a way that minimizes internal gaps while maintaining alignment. Using the @[minify] attribute may
cause issues with binary serialization or reflection. Be mindful of these potential side effects
when using this attribute.
When to Use
- When you want to minimize memory usage and you're not using binary serialization or reflection.
When to Avoid
- When using binary serialization or reflection, as it may cause unexpected behavior.
_likely_/_unlikely_
if _likely_(bool expression) { - hints to the C compiler, that the passed boolean expression is
very likely to be true, so it can generate assembly code, with less chance of branch misprediction.
In the JS backend, that does nothing.
if _unlikely_(bool expression) { is similar to _likely_(x), but it hints that the boolean
expression is highly improbable. In the JS backend, that does nothing.
When to Use
- In conditional statements where one branch is clearly more frequently executed than the other.
When to Avoid
- When the prediction can be wrong, as it might cause a performance penalty due to branch misprediction.
-skip-unused
This flag tells the V compiler to omit code that is not needed in the final executable to run your
program correctly. This will remove unneeded const arrays allocations and unused functions
from the code in the generated executable.
This flag will be on by default in the future when its implementation will be stabilized and all severe bugs will be found.
When to Use
- For production builds where you want to reduce the executable size and improve runtime performance.
When to Avoid
- Where it doesn't work for you.
-fast-math
This flag enables optimizations that disregard strict compliance with the IEEE standard for floating-point arithmetic. While this could lead to faster code, it may produce incorrect or less accurate mathematical results.
The full specter of math operations that -fast-math affects can be found
here.
When to Use
- In applications where performance is more critical than precision, like certain graphics rendering tasks.
When to Avoid
- In applications requiring strict mathematical accuracy, such as scientific simulations or financial calculations.
-d no_segfault_handler
Using this flag omits the segfault handler, reducing the executable size and potentially improving compile time. However, in the case of a segmentation fault, the output will not contain stack trace information, making debugging more challenging.
When to Use
- In small, well-tested utilities where a stack trace is not essential for debugging.
When to Avoid
- In large-scale, complex applications where robust debugging is required.
-cflags -march=native
This flag directs the C compiler to generate instructions optimized for the host CPU. This can improve performance but will produce an executable incompatible with other/older CPUs.
When to Use
- When the software is intended to run only on the build machine or in a controlled environment with identical hardware.
When to Avoid
- When distributing the software to users with potentially older CPUs.
-compress
This flag executes upx to compress the resultant executable, reducing its size by around 50%-70%.
The executable will be uncompressed at runtime, so it will take a bit more time to start.
It will also take extra RAM initially, as the compressed version of the app will be loaded into
memory, and then expanded to another chunk of memory.
Debugging such an application can be a bit harder, if you do not account for it.
Some antivirus programs also use heuristics, that trigger more often for compressed applications.
When to Use
- For really tiny environments, where the size of the executable on the file system, or when deploying is important (docker containers, rescue disks etc).
When to Avoid
- When you need to debug the application
- When the app's startup time is extremely important (where 1-2ms can be meaningful for you)
- When you can not afford to allocate more memory during application startup
- When you are deploying an app to users with antivirus software that could misidentify your app as malicious, just because it decompresses its code at runtime.
PGO (Profile-Guided Optimization)
PGO allows the compiler to optimize code based on its behavior during sample runs. This can improve performance and reduce the size of the output executable, but it adds complexity to the build process.
When to Use
- For performance-critical applications where the added build complexity is justifiable.
When to Avoid
- For small, short-lived, or rapidly-changing projects where the added build complexity isn't justified.
PGO with Clang
This is an example bash script you can use to optimize your CLI V program without user interactions. In most cases, you will need to change this script to make it suitable for your particular program.
#!/usr/bin/env bash
# Get the full path to the current directory
CUR_DIR=$(pwd)
# Remove existing PGO data
rm -f *.profraw
rm -f default.profdata
# Initial build with PGO instrumentation
v -cc clang -skip-unused -prod -cflags -fprofile-generate -o pgo_gen .
# Run the instrumented executable 10 times
for i in {1..10}; do
./pgo_gen
done
# Merge the collected data
llvm-profdata merge -o default.profdata *.profraw
# Compile the optimized version using the PGO data
v -cc clang -skip-unused -prod -cflags "-fprofile-use=${CUR_DIR}/default.profdata" -o optimized_program .
# Remove PGO data and instrumented executable
rm *.profraw
rm pgo_gen
Atomics
V has no special support for atomics, yet, nevertheless it's possible to treat variables as atomics
by calling C functions from V. The standard C11 atomic functions like atomic_store()
are usually defined with the help of macros and C compiler magic to provide a kind of
overloaded C functions.
Since V does not support overloading functions by intention there are wrapper functions defined in
C headers named atomic.h that are part of the V compiler infrastructure.
There are dedicated wrappers for all unsigned integer types and for pointers.
(u8 is not fully supported on Windows) – the function names include the type name
as suffix. e.g. C.atomic_load_ptr() or C.atomic_fetch_add_u64().
To use these functions the C header for the used OS has to be included and the functions that are intended to be used have to be declared. Example:
$if windows {
#include "@VEXEROOT/thirdparty/stdatomic/win/atomic.h"
} $else {
#include "@VEXEROOT/thirdparty/stdatomic/nix/atomic.h"
}
// declare functions we want to use - V does not parse the C header
fn C.atomic_store_u32(&u32, u32)
fn C.atomic_load_u32(&u32) u32
fn C.atomic_compare_exchange_weak_u32(&u32, &u32, u32) bool
fn C.atomic_compare_exchange_strong_u32(&u32, &u32, u32) bool
const num_iterations = 10000000
// see section "Global Variables" below
__global (
atom u32 // ordinary variable but used as atomic
)
fn change() int {
mut races_won_by_change := 0
for {
mut cmp := u32(17) // addressable value to compare with and to store the found value
// atomic version of `if atom == 17 { atom = 23 races_won_by_change++ } else { cmp = atom }`
if C.atomic_compare_exchange_strong_u32(&atom, &cmp, 23) {
races_won_by_change++
} else {
if cmp == 31 {
break
}
cmp = 17 // re-assign because overwritten with value of atom
}
}
return races_won_by_change
}
fn main() {
C.atomic_store_u32(&atom, 17)
t := spawn change()
mut races_won_by_main := 0
mut cmp17 := u32(17)
mut cmp23 := u32(23)
for i in 0 .. num_iterations {
// atomic version of `if atom == 17 { atom = 23 races_won_by_main++ }`
if C.atomic_compare_exchange_strong_u32(&atom, &cmp17, 23) {
races_won_by_main++
} else {
cmp17 = 17
}
desir := if i == num_iterations - 1 { u32(31) } else { u32(17) }
// atomic version of `for atom != 23 {} atom = desir`
for !C.atomic_compare_exchange_weak_u32(&atom, &cmp23, desir) {
cmp23 = 23
}
}
races_won_by_change := t.wait()
atom_new := C.atomic_load_u32(&atom)
println('atom: ${atom_new}, #exchanges: ${races_won_by_main + races_won_by_change}')
// prints `atom: 31, #exchanges: 10000000`)
println('races won by\n- `main()`: ${races_won_by_main}\n- `change()`: ${races_won_by_change}')
}
In this example both main() and the spawned thread change() try to replace a value of 17
in the global atom with a value of 23. The replacement in the opposite direction is
done exactly 10000000 times. The last replacement will be with 31 which makes the spawned
thread finish.
It is not predictable how many replacements occur in which thread, but the sum will always be 10000000. (With the non-atomic commands from the comments the value will be higher or the program will hang – dependent on the compiler optimization used.)
Global Variables
By default V does not allow global variables. However, in low level applications they have their
place so their usage can be enabled with the compiler flag -enable-globals.
Declarations of global variables must be surrounded with a __global ( ... )
specification – as in the example above.
An initializer for global variables must be explicitly converted to the
desired target type. If no initializer is given a default initialization is done.
Some objects like semaphores and mutexes require an explicit initialization in place, i.e.
not with a value returned from a function call but with a method call by reference.
A separate init() function can be used for this purpose – it will be called before main():
import sync
__global (
sem sync.Semaphore // needs initialization in `init()`
mtx sync.RwMutex // needs initialization in `init()`
f1 = f64(34.0625) // explicily initialized
shmap shared map[string]f64 // initialized as empty `shared` map
f2 f64 // initialized to `0.0`
)
fn init() {
sem.init(0)
mtx.init()
}
Be aware that in multi threaded applications the access to global variables is subject to race conditions. There are several approaches to deal with these:
- use
sharedtypes for the variable declarations and uselockblocks for access. This is most appropriate for larger objects like structs, arrays or maps. - handle primitive data types as "atomics" using special C-functions (see above).
- use explicit synchronization primitives like mutexes to control access. The compiler cannot really help in this case, so you have to know what you are doing.
- don't care – this approach is possible but makes only sense if the exact values
of global variables do not really matter. An example can be found in the
randmodule where global variables are used to generate (non cryptographic) pseudo random numbers. In this case data races lead to random numbers in different threads becoming somewhat correlated, which is acceptable considering the performance penalty that using synchronization primitives would represent.
Cross compilation
To cross compile your project simply run
v -os windows .
or
v -os linux .
Note
Cross-compiling a windows binary on a linux machine requires the GNU C compiler for MinGW-w64 (targeting Win64) to first be installed.
For Ubuntu/Debian based distributions:
sudo apt install gcc-mingw-w64-x86-64
For Arch based distributions:
sudo pacman -S mingw-w64-gcc
(Cross compiling for macOS is temporarily not possible.)
If you don't have any C dependencies, that's all you need to do. This works even
when compiling GUI apps using the ui module or graphical apps using gg.
You will need to install Clang, LLD linker, and download a zip file with libraries and include files for Windows and Linux. V will provide you with a link.
Debugging
C Backend binaries (Default)
To debug issues in the generated binary (flag: -b c), you can pass these flags:
-g- produces a less optimized executable with more debug information in it. V will enforce line numbers from the .v files in the stacktraces, that the executable will produce on panic. It is usually better to pass -g, unless you are writing low level code, in which case use the next option-cg.-cg- produces a less optimized executable with more debug information in it. The executable will use C source line numbers in this case. It is frequently used in combination with-keepc, so that you can inspect the generated C program in case of panic, or so that your debugger (gdb,lldbetc.) can show you the generated C source code.-showcc- prints the C command that is used to build the program.-show-c-output- prints the output, that your C compiler produced while compiling your program.-keepc- do not delete the generated C source code file after a successful compilation. Also keep using the same file path, so it is more stable, and easier to keep opened in an editor/IDE.
For best debugging experience if you are writing a low level wrapper for an existing
C library, you can pass several of these flags at the same time:
v -keepc -cg -showcc yourprogram.v, then just run your debugger (gdb/lldb) or IDE
on the produced executable yourprogram.
If you just want to inspect the generated C code,
without further compilation, you can also use the -o flag (e.g. -o file.c).
This will make V produce the file.c then stop.
If you want to see the generated C source code for just a single C function,
for example main, you can use: -printfn main -o file.c.
To debug the V executable itself you need to compile from src with ./v -g -o v cmd/v.
You can debug tests with for example v -g -keepc prog_test.v. The -keepc flag is needed,
so that the executable is not deleted, after it was created and ran.
To see a detailed list of all flags that V supports,
use v help, v help build and v help build-c.
Commandline Debugging
Troubleshooting (debugging) executables created with V in GDB
Visual debugging Setup:
Native Backend binaries
Currently there is no debugging support for binaries, created by the
native backend (flag: -b native).
Javascript Backend
To debug the generated Javascript output you can activate source maps:
v -b js -sourcemap hello.v -o hello.js
For all supported options check the latest help:
v help build-js
V and C
Calling C from V
V currently does not have a parser for C code. That means that even
though it allows you to #include existing C header and source files,
it will not know anything about the declarations in them. The #include
statement will only appear in the generated C code, to be used by the
C compiler backend itself.
Example of #include
#include <stdio.h>
After this statement, V will not know anything about the functions and
structs declared in stdio.h, but if you try to compile the .v file,
it will add the include in the generated C code, so that if that header file
is missing, you will get a C error (you will not in this specific case, if you
have a proper C compiler setup, since <stdio.h> is part of the
standard C library).
To overcome that limitation (that V does not have a C parser), V needs you to
redeclare the C functions and structs, on the V side, in your .c.v files.
Note that such redeclarations only need to have enough details about the
functions/structs that you want to use.
Note also that they do not have to be complete, unlike the ones in the .h files.
C. struct redeclarations For example, if a struct has 3 fields on the C side, but you want to only refer to 1 of them, you can declare it like this:
Example of C struct redeclaration
struct C.NameOfTheStruct {
a_field int
}
Another feature, that is very frequently needed for C interoperability,
is the @[typedef] attribute. It is used for marking C. structs,
that are defined with typedef struct SomeName { ..... } TypeName; in the C headers.
For that case, you will have to write something like this in your .c.v file:
@[typedef]
pub struct C.TypeName {
}
Note that the name of the C. struct in V, is the one after the struct SomeName {...}.
C. function redeclarations
The situation is similar for C. functions. If you are going to call just 1 function in a
library, but its .h header declares dozens of them, you will only need to declare that single
function, for example:
Example of C function redeclaration
fn C.name_of_the_C_function(param1 int, const_param2 &char, param3 f32) f64
... and then later, you will be able to call the same way you would V function:
f := C.name_of_the_C_function(123, c'here is some C style string', 1.23)
dump(f)
Example of using a C function from stdio, by redeclaring it on the V side
#include <stdio.h>
// int dprintf(int fd, const char *format, ...)
fn C.dprintf(fd int, const_format &char, ...voidptr) int
value := 12345
x := C.dprintf(0, c'Hello world, value: %d\n', value)
dump(x)
If your C backend compiler is properly setup, you should see something like this, when you try to run it:
#0 10:42:32 /v/examples> v run a.v
Hello world, value: 12345
[a.v:8] x: 26
#0 10:42:33 /v/examples>
Note, that the C function redeclarations look very simillar to the V ones, with some differences:
- They lack a body (they are defined on the C side) .
- Their names start with
C.. - Their names can have capital letters (unlike V ones, that are required to use snake_case) .
Note also the second parameter const char *format, which was redeclared as const_format &char .
The const_ prefix in that redeclaration may seem arbitrary, but it is important, if you want
to compile your code with -cstrict or thirdparty C static analysis tools. V currently does not
have another way to express that this parameter is a const (this will probably change in V 1.0).
For some C functions, that use variadics (...) as parameters, V supports a special syntax for
the parameters - ...voidptr, that is not available for ordinary V functions (V's variadics are
required to have the same exact type). Usually those are functions of the printf/scanf family
i.e for printf, fprintf, scanf, sscanf, etc, and other formatting/parsing/logging
functions.
Example
#flag freebsd -I/usr/local/include -L/usr/local/lib
#flag -lsqlite3
#include "sqlite3.h"
// See also the example from https://www.sqlite.org/quickstart.html
pub struct C.sqlite3 {
}
pub struct C.sqlite3_stmt {
}
type FnSqlite3Callback = fn (voidptr, int, &&char, &&char) int
fn C.sqlite3_open(&char, &&C.sqlite3) int
fn C.sqlite3_close(&C.sqlite3) int
fn C.sqlite3_column_int(stmt &C.sqlite3_stmt, n int) int
// ... you can also just define the type of parameter and leave out the C. prefix
fn C.sqlite3_prepare_v2(&C.sqlite3, &char, int, &&C.sqlite3_stmt, &&char) int
fn C.sqlite3_step(&C.sqlite3_stmt)
fn C.sqlite3_finalize(&C.sqlite3_stmt)
fn C.sqlite3_exec(db &C.sqlite3, sql &char, cb FnSqlite3Callback, cb_arg voidptr, emsg &&char) int
fn C.sqlite3_free(voidptr)
fn my_callback(arg voidptr, howmany int, cvalues &&char, cnames &&char) int {
unsafe {
for i in 0 .. howmany {
print('| ${cstring_to_vstring(cnames[i])}: ${cstring_to_vstring(cvalues[i]):20} ')
}
}
println('|')
return 0
}
fn main() {
db := &C.sqlite3(unsafe { nil }) // this means `sqlite3* db = 0`
// passing a string literal to a C function call results in a C string, not a V string
C.sqlite3_open(c'users.db', &db)
// C.sqlite3_open(db_path.str, &db)
query := 'select count(*) from users'
stmt := &C.sqlite3_stmt(unsafe { nil })
// Note: You can also use the `.str` field of a V string,
// to get its C style zero terminated representation
C.sqlite3_prepare_v2(db, &char(query.str), -1, &stmt, 0)
C.sqlite3_step(stmt)
nr_users := C.sqlite3_column_int(stmt, 0)
C.sqlite3_finalize(stmt)
println('There are ${nr_users} users in the database.')
error_msg := &char(0)
query_all_users := 'select * from users'
rc := C.sqlite3_exec(db, &char(query_all_users.str), my_callback, voidptr(7), &error_msg)
if rc != C.SQLITE_OK {
eprintln(unsafe { cstring_to_vstring(error_msg) })
C.sqlite3_free(error_msg)
}
C.sqlite3_close(db)
}
Calling V from C
Since V can compile to C, calling V code from C is very easy, once you know how.
Use v -o file.c your_file.v to generate a C file, corresponding to the V code.
More details in call_v_from_c example.
Passing C compilation flags
Add #flag directives to the top of your V files to provide C compilation flags like:
-Ifor adding C include files search paths-lfor adding C library names that you want to get linked-Lfor adding C library files search paths-Dfor setting compile time variables
You can (optionally) use different flags for different targets.
Currently the linux, darwin , freebsd, and windows flags are supported.
Note
Each flag must go on its own line (for now)
#flag linux -lsdl2
#flag linux -Ivig
#flag linux -DCIMGUI_DEFINE_ENUMS_AND_STRUCTS=1
#flag linux -DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1
#flag linux -DIMGUI_IMPL_API=
In the console build command, you can use:
-ccto change the default C backend compiler.-cflagsto pass custom flags to the backend C compiler (passed before other C options).-ldflagsto pass custom flags to the backend C linker (passed after every other C option).- For example:
-cc gcc-9 -cflags -fsanitize=thread.
You can define a VFLAGS environment variable in your terminal to store your -cc
and -cflags settings, rather than including them in the build command each time.
#pkgconfig
Add #pkgconfig directives to tell the compiler which modules should be used for compiling
and linking using the pkg-config files provided by the respective dependencies.
As long as backticks can't be used in #flag and spawning processes is not desirable for security
and portability reasons, V uses its own pkgconfig library that is compatible with the standard
freedesktop one.
If no flags are passed it will add --cflags and --libs to pkgconfig (not to V).
In other words, both lines below do the same:
#pkgconfig r_core
#pkgconfig --cflags --libs r_core
The .pc files are looked up into a hardcoded list of default pkg-config paths, the user can add
extra paths by using the PKG_CONFIG_PATH environment variable. Multiple modules can be passed.
To check the existence of a pkg-config use $pkgconfig('pkg') as a compile time "if" condition to
check if a pkg-config exists. If it exists the branch will be created. Use $else or $else $if
to handle other cases.
$if $pkgconfig('mysqlclient') {
#pkgconfig mysqlclient
} $else $if $pkgconfig('mariadb') {
#pkgconfig mariadb
}
Including C code
You can also include C code directly in your V module. For example, let's say that your C code is located in a folder named 'c' inside your module folder. Then:
- Put a v.mod file inside the toplevel folder of your module (if you
created your module with
v newyou already have v.mod file). For example:
Module {
name: 'mymodule',
description: 'My nice module wraps a simple C library.',
version: '0.0.1'
dependencies: []
}
- Add these lines to the top of your module:
#flag -I @VMODROOT/c
#flag @VMODROOT/c/implementation.o
#include "header.h"
Note
@VMODROOT will be replaced by V with the nearest parent folder, where there is a v.mod file. Any .v file beside or below the folder where the v.mod file is, can use
#flag @VMODROOT/abcto refer to this folder. The @VMODROOT folder is also prepended to the module lookup path, so you can import other modules under your @VMODROOT, by just naming them.
The instructions above will make V look for an compiled .o file in
your module folder/c/implementation.o.
If V finds it, the .o file will get linked to the main executable, that used the module.
If it does not find it, V assumes that there is a @VMODROOT/c/implementation.c file,
and tries to compile it to a .o file, then will use that.
This allows you to have C code, that is contained in a V module, so that its distribution is easier. You can see a complete minimal example for using C code in a V wrapper module here: project_with_c_code. Another example, demonstrating passing structs from C to V and back again: interoperate between C to V to C.
C types
Ordinary zero terminated C strings can be converted to V strings with
unsafe { &char(cstring).vstring() } or if you know their length already with
unsafe { &char(cstring).vstring_with_len(len) }.
Note
The
.vstring()and.vstring_with_len()methods do NOT create a copy of thecstring, so you should NOT free it after calling the method.vstring(). If you need to make a copy of the C string (some libc APIs likegetenvpretty much require that, since they return pointers to internal libc memory), you can usecstring_to_vstring(cstring).
On Windows, C APIs often return so called wide strings (utf16 encoding).
These can be converted to V strings with string_from_wide(&u16(cwidestring)) .
V has these types for easier interoperability with C:
voidptrfor C'svoid*,&u8for C'sbyte*and&charfor C'schar*.&&charfor C'schar**
To cast a voidptr to a V reference, use user := &User(user_void_ptr).
voidptr can also be dereferenced into a V struct through casting: user := User(user_void_ptr).
an example of a module that calls C code from V
C Declarations
C identifiers are accessed with the C prefix similarly to how module-specific
identifiers are accessed. Functions must be redeclared in V before they can be used.
Any C types may be used behind the C prefix, but types must be redeclared in V in
order to access type members.
To redeclare complex types, such as in the following C code:
struct SomeCStruct {
uint8_t implTraits;
uint16_t memPoolData;
union {
struct {
void* data;
size_t size;
};
DataView view;
};
};
members of sub-data-structures may be directly declared in the containing struct as below:
pub struct C.SomeCStruct {
implTraits u8
memPoolData u16
// These members are part of sub data structures that can't currently be represented in V.
// Declaring them directly like this is sufficient for access.
// union {
// struct {
data voidptr
size usize
// }
view C.DataView
// }
}
The existence of the data members is made known to V, and they may be used without re-creating the original structure exactly.
Alternatively, you may embed the sub-data-structures to maintain a parallel code structure.
Export to shared library
By default all V functions have the following naming scheme in C: [module name]__[fn_name].
For example, fn foo() {} in module bar will result in bar__foo().
To use a custom export name, use the @[export] attribute:
@[export: 'my_custom_c_name']
fn foo() {
}
Translating C to V
V can translate your C code to human readable V code, and generating V wrappers on top of C libraries.
C2V currently uses Clang's AST to generate V, so to translate a C file to V you need to have Clang installed on your machine.
Let's create a simple program test.c first:
#include "stdio.h"
int main() {
for (int i = 0; i < 10; i++) {
printf("hello world\n");
}
return 0;
}
Run v translate test.c, and V will generate test.v:
fn main() {
for i := 0; i < 10; i++ {
println('hello world')
}
}
To generate a wrapper on top of a C library use this command:
v translate wrapper c_code/libsodium/src/libsodium
This will generate a directory libsodium with a V module.
Example of a C2V generated libsodium wrapper:
https://github.com/vlang/libsodium
When should you translate C code and when should you simply call C code from V?
If you have well-written, well-tested C code, then of course you can always simply call this C code from V.
Translating it to V gives you several advantages:
- If you plan to develop that code base, you now have everything in one language, which is much safer and easier to develop in than C.
- Cross-compilation becomes a lot easier. You don't have to worry about it at all.
- No more build flags and include files either.
Working around C issues
In some cases, C interop can be extremely difficult. One of these such cases is when headers conflict with each other. For example, V needs to include the Windows header libraries in order for your V binaries to work seamlessly across all platforms.
However, since the Windows header libraries use extremely generic names such as Rectangle,
this will cause a conflict if you wish to use C code that also has a name defined as Rectangle.
For very specific cases like this, we have #preinclude.
This will allow things to be configured before V adds in its built in libraries.
Example usage:
// This will include before built in libraries are used.
#preinclude "pre_include.h"
// This will include after built in libraries are used.
#include "include.h"
An example of what might be included in pre_include.h
can be found here
This is an advanced feature, and will not be necessary outside of very specific cases with C interop, meaning it could cause more issues than it solves.
Consider it last resort!
Other V Features
Inline assembly
a := 100
b := 20
mut c := 0
asm amd64 {
mov eax, a
add eax, b
mov c, eax
; =r (c) as c // output
; r (a) as a // input
r (b) as b
}
println('a: ${a}') // 100
println('b: ${b}') // 20
println('c: ${c}') // 120
For more examples, see vlib/v/slow_tests/assembly/asm_test.amd64.v
Hot code reloading
module main
import time
@[live]
fn print_message() {
println('Hello! Modify this message while the program is running.')
}
fn main() {
for {
print_message()
time.sleep(500 * time.millisecond)
}
}
Build this example with v -live message.v.
You can also run this example with v -live run message.v.
Make sure that in command you use a path to a V's file,
not a path to a folder (like v -live run .) -
in that case you need to modify content of a folder (add new file, for example),
because changes in message.v will have no effect.
Functions that you want to be reloaded must have @[live] attribute
before their definition.
Right now it's not possible to modify types while the program is running.
More examples, including a graphical application: github.com/vlang/v/tree/master/examples/hot_reload.
About keeping states in hot reloading functions with v -live run
V's hot code reloading relies on marking the functions that you want to reload with @[live],
then compiling a shared library of these @[live] functions, and then
your v program loads that shared library at runtime.
V (with the -live option) starts a new thread, that monitors the source files for changes, and when it detects modifications, it recompiles the shared library, and reloads it at runtime, so that new calls to those @[live] functions will be made to the newly loaded library.
It keeps all the accumulated state (from locals outside the @[live] functions, from heap variables and from globals), allowing to tweak the code in the merged functions quickly.
When there are more substantial changes (to data structures, or to functions that were not marked), you will have to restart the running app manually.
Cross-platform shell scripts in V
V can be used as an alternative to Bash to write deployment scripts, build scripts, etc.
The advantage of using V for this, is the simplicity and predictability of the language, and cross-platform support. "V scripts" run on Unix-like systems, as well as on Windows.
To use V's script mode, save your source file with the .vsh file extension.
It will make all functions in the os module global (so that you can use mkdir() instead
of os.mkdir(), for example).
V also knows to compile & run .vsh files immediately, so you do not need a separate
step to compile them. V will also recompile an executable, produced by a .vsh file,
only when it is older than the .vsh source file, i.e. runs after the first one, will
be faster, since there is no need for a re-compilation of a script, that has not been changed.
An example deploy.vsh:
#!/usr/bin/env -S v -gc none -cc tcc -d use_openssl -enable-globals run
// Note: The shebang line above, associates the .vsh file to V on Unix-like systems,
// so it can be run just by specifying the path to the .vsh file, once it's made
// executable, using `chmod +x deploy.vsh`, i.e. after that chmod command, you can
// run the .vsh script, by just typing its name/path like this: `./deploy.vsh`
// print command then execute it
fn sh(cmd string) {
println('❯ ${cmd}')
print(execute_or_exit(cmd).output)
}
// Remove if build/ exits, ignore any errors if it doesn't
rmdir_all('build') or {}
// Create build/, never fails as build/ does not exist
mkdir('build')!
// Move *.v files to build/
result := execute('mv *.v build/')
if result.exit_code != 0 {
println(result.output)
}
sh('ls')
// Similar to:
// files := ls('.')!
// mut count := 0
// if files.len > 0 {
// for file in files {
// if file.ends_with('.v') {
// mv(file, 'build/') or {
// println('err: ${err}')
// return
// }
// }
// count++
// }
// }
// if count == 0 {
// println('No files')
// }
Now you can either compile this like a normal V program and get an executable you can deploy and run
anywhere:
v -skip-running deploy.vsh && ./deploy
Or run it like a traditional Bash script:
v run deploy.vsh (or simply just v deploy.vsh)
On Unix-like platforms, the file can be run directly after making it executable using chmod +x:
./deploy.vsh
Vsh scripts with no extension
Whilst V does normally not allow vsh scripts without the designated file extension, there is a way
to circumvent this rule and have a file with a fully custom name and shebang. Whilst this feature
exists it is only recommended for specific usecases like scripts that will be put in the path and
should not be used for things like build or deploy scripts. To access this feature start the
file with #!/usr/bin/env -S v -gc none -cc tcc -d use_openssl -enable-globals run the built executable. This will run in crun mode so it will only rebuild if changes to the script were made and keep the binary as tmp.. **Caution**: if this filename already exists the file will be overridden. If you want to rebuild each time and not keep this binary instead use #!/usr/bin/env -S v -gc none -cc tcc -d use_openssl -enable-globals run
Appendices
Appendix I: Keywords
V has 44 reserved keywords (3 are literals):
as
asm
assert
atomic
break
const
continue
defer
else
enum
false
fn
for
go
goto
if
import
in
interface
is
isreftype
lock
match
module
mut
none
or
pub
return
rlock
select
shared
sizeof
spawn
static
struct
true
type
typeof
union
unsafe
volatile
__global
__offsetof
See also V Types.
Appendix II: Operators
This lists operators for primitive types only.
+ sum integers, floats, strings
- difference integers, floats
* product integers, floats
/ quotient integers, floats
% remainder integers
~ bitwise NOT integers
& bitwise AND integers
| bitwise OR integers
^ bitwise XOR integers
! logical NOT bools
&& logical AND bools
|| logical OR bools
!= logical XOR bools
<< left shift integer << unsigned integer
>> right shift integer >> unsigned integer
>>> unsigned right shift integer >> unsigned integer
Precedence Operator
5 * / % << >> >>> &
4 + - | ^
3 == != < <= > >=
2 &&
1 ||
Assignment Operators
+= -= *= /= %=
&= |= ^=
>>= <<= >>>=
&&= ||=