编译时代码

按照V语言作者的说法,为了保持V语言的简单,不会加入像C语言那样的预处理器,而是通过编译时代码来实现类似的功能。

编译时就是在编译阶段,根据编译时代码,动态生成代码。编译时代码使用最多的场景就是动态生成指定平台的代码,其他平台的代码不会生成。

V语言中编译时代码以$开头。

条件编译

条件编译是编译时代码最主要的应用场景:根据编译时条件,动态生成指定平台的代码,其他平台的代码不会生成,在C语言中使用了预处理器来实现。

目前的条件编译有2种主要方式:

1.根据源文件名后缀来实现条件编译

2.根据代码中的$if来实现条件编译

按源文件后缀名进行条件编译

源文件后缀包含了2个维度的条件编译:

  • 通用源文件

    后缀名编译条件

    只有 .v

    所有操作系统,所有后端都参与编译

  • 操作系统(os)

    后缀名编译条件

    _default

    默认的,表示所有操作系统都参与编译,比如file_default.c.v.同个目录中,如果同时存在默认的和平台特有的,平台特有的文件会参与编译,默认的被忽略

    _nix

    linux,unix,darwin,solaris下才会参与编译,或者说是非windows

    _macos或 _darwin

    mac下才会参与编译

    _linux

    linux下才会参与编译

    _solaris

    solaris下才会参与编译

    _plan9

    plan9下才会参与编译

    _windows

    windows下才会参与编译

    _android

    android平台下才会参与编译

    _ios

    ios平台下才会参与编译

    _bare

    编译成裸机(metal)环境运行代码,才会参与编译,不进行排列组合

  • 编译器后端(backend)

    后缀名编译器后端

    .c

    C语言为编译器后端时才会参与编译

    .js

    js语言为编译器后端时才会参与编译

    .native

    直接生成x64机器码时才会参与编译

    以上2个维度排列组合:

    file.v	#所有操作系统,所有编译后端都参与编译
    file.c.v	#所有操作系统,C编译后端才参与编译,不会被特定平台覆盖,而是都编译
    file.js.v	#所有操作系统,js编译后端才参与编译,不会被特定平台覆盖,而是都编译
    file.native.v #所有操作系统,native编译后端才参与编译
    
    file_default.c.v #同目录如果存在特定平台的C后端文件,此文件会被忽略,不参与编译
    file_linux.c.v
    file_macos.c.v
    file_windows.c.v
    file_windows.js.v
    file_windows.x64.v
    ...

    正常情况下,在同一个模块中函数是不允许重复定义的,但是可以在.c.v或.js.v重复定义.v已经定义过的函数,.c.v或.js.v的同名函数会优先被执行,也就是覆盖了.v定义的函数。

    这样的好处是可以在.v定义通用版本的函数,在.c.v定义针对C后端的函数,在.js.v定义针对js后端的函数。

    mymodule/wrapper.v

    module mymodule
    
    pub fn value() int {
    	return 666
    }

    mymodule/wrapper.c.v

    module mymodule
    
    pub fn value() int {
    	return 123
    }

    mymodule/wrapper.js.v

    module mymodule
    
    pub fn value() int {
    	return 456
    }

    主模块的main.v

    module main
    
    import mymodule
    
    fn main() {
    	println(mymodule.value())	//C编译器后端输出123
    }

条件编译选项

内置条件编译选项

以下内置的条件编译选项,可以在代码中使用:

OSCompilersPlatformsOther

windows, linux, macos,plan9

gcc, tinyc

amd64, aarch64

debug, prod, test

mac, darwin, ios,

clang, mingw

x64, x32

js, glibc, prealloc

android,mach, dragonflytermux

msvc

little_endian

no_bounds_checking

gnu, hpux, haiku, qnx

cplusplus

big_endian

freestanding

solaris, linux_or_macos

fn main() {
	$if windows {
		println('windows')
		$if msvc { // 可以在windows平台中,进一步判断是msvc还是mingw
		}
		$if mingw { // 可以在windows平台中,进一步判断是msvc还是mingw
		}
	}
	$if linux {
		println('linux')
	}
	$if macos { // 或者mac
		println('mac')
	}
	$if windows {
	} $else $if macos { // else if分支
		println('macos')
	} $else $if linux { // else if分支
		println('linux')
	} $else { // else分支
		println('others')
	}
	$if termux { // 在安卓终端模拟器termux环境中
		println('termux')
	}
	$if !windows { // 使用非运算
	}
	$if linux || macos { // 使用或运算符
	}
	$if linux && x64 { // 使用且运算符
	}
	// 其他条件编译的选项有:
	// freebsd,openbsd,netbsd,bsd,dragonfly,android,solaris
	// js,tinyc,clang,msvc,mingw
}

判断是否使用了-cg,进入调试模式

$if debug {
	println('from debug')
}
//执行:
//v run main.v -cg

判断是否在测试代码中执行

fn test_comptime_if_test() {
	mut i := 0
	$if test { //如果在测试函数中,则执行
		i++
	}
	$if !test { //非测试函数中
		i--
	}
	assert i == 1
}

判断平台是32位还是64位

fn main() {
	mut x := 0
	$if x32 {
		println('system is 32 bit')
		x = 1
	}
	$if x64 {
		println('system is 64 bit')
		x = 2
	}
}

判断平台使用的字节序是小字节序,还是大字节序

fn main() {
	mut x := 0
	$if little_endian { //小字节序
		println('system is little endian')
		x = 1
	}
	$if big_endian { //大字节序
		println('system is big endian')
		x = 2
	}
}

判断是否-prod生产编译

fn main() {
	$if prod {
		println('prod')
	} $else {
		println('not prod')
	}
}

自定义编译选项

除了内置的条件编译选项,也可以识别自定义条件编译选项。

使用-d或-define来自定义编译选项,并且可以在代码中接收选项的传入值。

要特别注意的是:自定义条件编译选项名后面一定要加一个问号。

//main.v
module main

fn main() {
	$if abc ? {  //如果编译时没有传递abc编译选项,这段代码就不会被生成C代码
		println('自定义编译选项abc存在')
	}
}

编译时,传递自定义条件编译变量:

v -d abc main.v
v -define abc main.v
#执行main时,输出:自定义编译选项abc存在

同时使用多个自定义编译选项:

module main

fn main() {
	$if time_v ? {
		println('time_v')
	}
	$if abc ? { 
		println('abc')
	}
	$if value ? {
		println('1')
	}
}
v  -d abc -d time_v main.v	#增加自定义编译选项abc和time_v,类型默认为bool类型,默认值为true
v  -d abc -d time_v -d value='0' main.v #也可以使用‘1’或‘0’,等价于true和false
./main 	#输出time_v和abc

编译时反射

$for用来实现反射的效果,目前只实现了结构体的反射,可以在运行时获得某一个结构体所有字段和方法的信息。

遍历结构体字段,返回字段信息数组:[]FieldData

// FieldData holds information about a field. Fields reside on structs.
pub struct FieldData {
pub:
	name          string // the name of the field f
	typ           int    // the internal TypeID of the field f,
	unaliased_typ int    // if f's type was an alias of int, this will be TypeID(int)
	//
	attrs  []string // the attributes of the field f
	is_pub bool     // f is in a `pub:` section
	is_mut bool     // f is in a `mut:` section
	//
	is_shared bool // `f shared Abc`
	is_atomic bool // `f atomic int` , TODO
	is_option bool // `f ?string` , TODO
	//
	is_array  bool // `f []string` , TODO
	is_map    bool // `f map[string]int` , TODO
	is_chan   bool // `f chan int` , TODO
	is_enum   bool // `f Enum` where Enum is an enum
	is_struct bool // `f Abc` where Abc is a struct , TODO
	is_alias  bool // `f MyInt` where `type MyInt = int`, TODO
	//
	indirections u8 // 0 for `f int`, 1 for `f &int`, 2 for `f &&int` , TODO
}

遍历结构体方法,返回方法信息数组:[]FunctionData

FunctionData {
    name: 'int_method1' //方法名称
    attrs: []						//方法注解
    args: []						//方法参数
    return_type: 7				//方法返回类型
    typ: 0							//未知
}
struct App {
	a string
	b string
mut:
	c int
	d f32
pub:
	e f32
	f u64
pub mut:
	g string
	h u8
}

@['foo/bar/three']
fn (mut app App) run() {
}

@['attr2']
fn (mut app App) method2() {
}

fn (mut app App) int_method1() int {
	return 0
}

fn (mut app App) int_method2() int {
	return 1
}

fn (mut app App) string_arg(x string) {
}

fn no_lines(s string) string {
	return s.replace('\n', ' ')
}

fn f1() {
	println(@FN)
	methods := ['run', 'method2', 'int_method1', 'int_method2', 'string_arg']
	$for method in App.methods { //遍历结构体所有方法
		println('  method: $method.name | ' + no_lines('$method'))
		assert method.name in methods
	}
}

fn f2() {
	println(@FN)
	$for method in App.methods { //遍历结构体所有方法
		println('  method: ' + no_lines('$method'))
		$if method.typ is fn () {
			assert method.name in ['run', 'method2']
		}
		$if method.return_type is int {
			assert method.name in ['int_method1', 'int_method2']
		}
		$if method.args[0].typ is string {
			assert method.name == 'string_arg'
		}
	}
}

fn main() {
	println(@FN)
	$for field in App.fields { //遍历结构体所有字段
		println('  field: $field.name | ' + no_lines('$field'))
		$if field.typ is string {
			assert field.name in ['a', 'b', 'g']
		}
		$if field.typ is f32 {
			assert field.name in ['d', 'e']
		}
		if field.is_mut {
			assert field.name in ['c', 'd', 'g', 'h']
		}
		if field.is_pub {
			assert field.name in ['e', 'f', 'g', 'h']
		}
		if field.is_pub && field.is_mut {
			assert field.name in ['g', 'h']
		}
	}
	f1()
	f2()
}

编译时动态字段赋值

struct Foo {
	immutable int
mut:
	test string
	name string
}

fn comptime_field_selector_read[T]() []string {
	mut t := T{}
	t.name = '2'
	t.test = '1'
	mut value_list := []string{}
	$for f in T.fields {
		$if f.typ is string {
			value_list << t.$f.name
		}
	}
	return value_list
}

fn test_comptime_field_selector_read() {
	assert comptime_field_selector_read[Foo]() == ['1', '2']
}

fn comptime_field_selector_write[T]() T {
	mut t := T{}
	$for f in T.fields {
		$if f.typ is string {
			t.$f.name = '1'
		}
		$if f.typ is int {
			t.$f.name = 1
		}
	}
	return t
}

fn test_comptime_field_selector_write() {
	res := comptime_field_selector_write[Foo]()
	assert res.immutable == 1
	assert res.test == '1'
	assert res.name == '1'
}

struct Foo2 {
	f Foo
}

fn nested_with_parentheses[T]() T {
	mut t := T{}
	$for f in T.fields {
		$if f.typ is Foo {
			t.$(f.name).test = '1'
		}
	}
	return t
}

fn test_nested_with_parentheses() {
	res := nested_with_parentheses[Foo2]()
	assert res.f.test == '1'
}

编译时获取结构体字段信息

可以在$for循环中获取结构体字段的信息:

module main

struct Abc {
	x    int
	y    int
	name string
}

struct MyStruct {
	s        string     [primary; sql: serial]
	i        int
	ch_i     chan int
	atomic_i atomic int
	//
	pointer1_i &int   = unsafe { nil }
	pointer2_i &&int  = unsafe { nil }
	pointer3_i &&&int = unsafe { nil }
	//
	array_i          []int
	map_i            map[int]int
	my_struct        Abc
	my_struct_shared shared Abc
	//
	o_s          ?string
	o_i          ?int
	o_ch_i       ?chan int = chan int{cap: 10}
	o_pointer1_i ?&int     = unsafe { nil }
	o_pointer2_i ?&&int    = unsafe { nil }
	o_pointer3_i ?&&&int   = unsafe { nil }
	//
	o_array_i          ?[]int
	o_map_i            ?map[int]int
	o_my_struct        ?Abc
	o_my_struct_shared ?shared Abc
}

fn main() {
	$for field in MyStruct.fields {
		println("field: ${field.name}")
		//使用内置函数可以得到的字段信息
		println(sizeof(field)) //字段内存大小
		println(typeof(field).idx) //字段类型的id
		println(typeof(field).name) //字段类型名字
		println(isreftype(field)) //字段类型是否为引用类型

		//使用FieldData可以得到的字段信息
		println(field.name) //字段名字
		println(field.typ) //字段的类型id
		println(field.unaliased_typ) //如果字段类型是类型别名,返回类型别名最原始类型的类型id
    
		println(field.attrs) //字段的属性注解
		println(field.is_pub) //字段是否为pub
		println(field.is_mut) //字段是否为mut
		println(field.is_shared) //字段是否为share
		println(field.is_atomic) //字段是否为atomic
		println(field.is_option) //字段是否为可选字段,带?
		println(field.is_array) //字段是否为数组
		println(field.is_map) //字段是否为字典
		println(field.is_chan) //字段是否为chan类型
		println(field.is_struct) //字段是否为结构体类型
 		println(field.is_alias) //字段是否为类型别名
		println(field.is_enum)  //字段是否为枚举类型
		println(field.indirections) //字段是否为指针类型,且是几级指针:0表示非指针,1表示一级指针,2表示2级指针...

		println('---')
	}
}

可获取的字段的信息,查看vlib/builtin.v源文件中的FieldData结构体:

pub struct FieldData {
pub:
	name          string // the name of the field f
	typ           int    // the internal TypeID of the field f,
	unaliased_typ int    // if f's type was an alias of int, this will be TypeID(int)
	//
	attrs  []string // the attributes of the field f
	is_pub bool     // f is in a `pub:` section
	is_mut bool     // f is in a `mut:` section
	//
	is_shared   bool // `f shared Abc`
	is_atomic   bool // `f atomic int` , TODO
	is_optional bool // `f ?string` , TODO
	//
	is_array  bool // `f []string` , TODO
	is_map    bool // `f map[string]int` , TODO
	is_chan   bool // `f chan int` , TODO
	is_struct bool // `f Abc` where Abc is a struct , TODO
	is_alias  bool // `f MyInt` where `type MyInt = int`, TODO
	//
	indirections u8 // 0 for `f int`, 1 for `f &int`, 2 for `f &&int` , TODO
}

获取结构体选项类型字段值

struct FixedStruct {
	a int
	b string
	c ?int
	d ?string
}

struct Encoder {}

fn main() {
	fixed := FixedStruct{123, '456', 789, '321'}
	println(fixed.a.str())
	println(fixed.c?.str()) //返回选项类型字段的值

	println(fixed.b.int())
	println(fixed.d?.int()) //返回选项类型字段的值

	e := Encoder{}
	e.encode_struct(fixed)
}

fn (e &Encoder) encode_struct[T](val T) {
	$for field in T.fields {
		mut value := val.$(field.name) //动态获取结构体字段的值
		$if field.is_option {
			println('>> ${value ?.str()}') //动态输出结构体选项字段的值
			println(val.$(field.name) ?.str()) //动态输出结构体选项字段的值
		}
	}
}

编译时动态调用方法

struct TestStruct {}

fn (t TestStruct) one_arg(a string) {
	println(a)
}

fn (t TestStruct) two_args(a string, b int) {
	println('$a:$b')
}

fn main() {
	t := TestStruct{}
	$for method in TestStruct.methods { // 获取结构体的所有方法
    	if method.name == 'two_args' { //当前方法的名字
      	  t.$method('hello', 42) // 动态调用方法
    	}
	}
}
struct MyStruct {}

struct TestStruct {}

fn (t TestStruct) three_args(m MyStruct, arg1 string, arg2 int) {
	println('$arg1,$arg2')
}

fn main() {
	t := TestStruct{}
	m := MyStruct{}
	args := ['one' '2'] //数组参数
	$for method in TestStruct.methods {
    	if method.name == 'three_args' {
        	t.$method(m, args) //动态调用方法时,也可以传递一个数组,会自动解构展开
    	}
	}
}

编译时判断泛型类型

可以使用编译时判断泛型的具体类型:

module main

fn kind[T]() {
	$if T is $int {
		println('int')
	}
	$if T is $float {
		println('float')
	}
	$if T is $array {
		println('array')
	}
	$if T is $map {
		println('map')
	}
	$if T is $struct {
		println('struct')
	}
	$if T is $interface {
		println('interface')
	}
	$if T is $enum {
		println('enum')
	}
	$if T is $sumtype {
		println('sumtype')
	}
	$if T is $function {
		println('function')
	}
	$if T is $alias {
		println('alias')
	}
	$if T is $option {
		println('option')
	}
}

fn kind_detail[T]() {
	$if T is u8 {
		println('u8')
	}
	$if T is int {
		println('int')
	}
	$if T is ?int {
		println('?int')
	}
	// $if T is !int {
	// 	println('!int')
	// }
	$if T in [u8, int] {
		println('u8 or int')
	}
	$if T !in [u8, int] {
		println('not u8 or int')
	}
	$if T in [int, $int] {
		println('int or Int')
	}
	$if T in [$sumtype, $map] {
		println('Sumtype or Map')
	}
	$if T in [Abc, Def] {
		println('Abc or Def')
	}
}

struct Abc {}

interface Def {}

enum Xyz {
	x
	y
	z
}

type Sumtype = Abc | f32 | int

struct GenericStruct[T] {
	x T
}

pub fn (s GenericStruct[T]) m() {
	$if T is $int {
		println('Int in method')
	}

	$if T is $array {
		println('Array in method')
	} $else {
		println('other')
	}
}

fn main() {
	// int
	kind[i8]()
	kind[i16]()
	kind[int]()
	kind[i64]()
	// int
	kind[u8]()
	kind[u16]()
	kind[u32]()
	kind[u64]()
	// float
	kind[f32]()
	kind[f64]()
	// array
	kind[[]int]()
	// map
	kind[map[string]string]()
	// struct
	kind[Abc]()
	// interface
	kind[Def]()
	// enum
	kind[Xyz]()
	// sumtype
	kind[Sumtype]()
	// generic struct
	s1 := GenericStruct[int]{}
	s1.m()
	s2 := GenericStruct[[]string]{}
	s2.m()
	s3 := GenericStruct[f32]{}
	s3.m()
	kind_detail[Abc]()
}

编译时全局变量

内置了开发和测试时需要的几个编译时全局变量,方便编译和测试使用:

module main

fn main() {
	println('module: ${@MOD}')					//当前模块
	println('fn: ${@FN}')						//当前函数
	println('sturct: ${@STRUCT}')				//当前结构体
	println('method: ${@METHOD}')				//当前方法
  
	println('file: ${@FILE}')					//当前源代码文件名
	println('line: ${@LINE}')					//当前代码所在的行
	println('column: ${@COLUMN}')				//当前代码在当前行中的列数
    println('location:${@LOCATION}')            //当前位置,包括当前文件,当前行,当前模块,当前函数
  
	println('vhash: ${@VHASH}')					//当前V编译器的hash版本号	
 	println('vexe: ${@VEXE}')					//当前V编译器命令行文件
	println('vexeroot: ${@VEXEROOT}')			//当前V编译器命令行所在的目录
	
	println('vmod_file: ${@VMOD_FILE}')	//当前文件所处项目的v.mod文件内容
	println('vmodroot: ${@VMODROOT}')		//当前文件所处项目的v.mod文件所在的目录
}

编译时获取环境变量

可以使用$env()编译时函数获取环境变量。运行时代码中os.get_env()函数也可以实现相同的效果。

比较特别的是$env()也可以在#flay和#include等C宏中使用,让C宏定义更灵活。

module main

//可以在C宏语句中使用,让C宏的定义更灵活
#flag linux -I $env('JAVA_HOME')/include

fn main() {
	compile_time_env := $env('PATH')
	println(compile_time_env)
}

编译时获取pkgconfig配置文件

可以使用$pkgconfig()编译时函数,来判断配置文件是否存在。

具体代码参考:集成C代码库章节

编译时嵌入静态文件

可以使用$embed_file()编译时函数,把各种类型的文件在编译时嵌入到二进制文件中,更方便编译打包成单一可执行文件,方便部署,目前vweb框架中有使用到。

$embed_file()函数返回的类型为EmbedFileData,详细的使用可参考:vlib/v/embed_file/embed_file.v源文件。

如果没有特别指定,使用-prod进行生产编译时,$embed_file()函数会使用zlib对嵌入二进制的静态文件进行压缩。

module main

import os

fn main() {
	mut embedded_file := $embed_file('v.png', .zlib) //可以使用zlib进行压缩
	mut fw := os.create('exported.png') or { panic(err) }
	// data函数返回字节数据,len是字节长度
	unsafe { fw.write_ptr(embedded_file.data(), embedded_file.len) } //使用data函数获取数据
	fw.close()
}

编译时模板渲染

V语言内置了一个简单的txt和html模板,可以通过$tmpl编译时函数,进行渲染。

fn build() string {
	name := 'Peter'
	age := 25
	numbers := [1, 2, 3]
	return $tmpl('a.txt') //读取模板文件,然后把变量替换到模板中对应的同名变量
}

fn main() {
	println(build())
}

a.txt 模板文件内容

name: @name //@开头的都是模板文件中需要替换的同名变量

age: @age

numbers: @numbers

@for number in numbers //也可以遍历数组类型变量
  @number
@end

编译时解析v.mod文件

在编译阶段,如果需要解析v.mod的文件内容,可以导入v.mod模块,解析v.mod文件的内容。

import v.vmod
vm := vmod.decode( @VMOD_FILE ) or { panic(err) }
eprintln('$vm.name $vm.version\n $vm.description')

编译时错误与警告

V语言内置2个编译时函数:compile_error和compile_warn,专门用来抛出编译时的错误和警告。

module main

fn main() {
	$if !linux {
   		 $compile_warn('这是编译时警告!') //抛出编译警告,会继续编译,执行
	}
	println('警告后代码')

	$if !macos {
		$compile_error('这是个编译时错误!') //抛出编译错误,终止编译,执行
	}
}

最后更新于