集成C代码库

优势

V的代码库很多都直接调用C标准库函数来实现,对C标准库的依赖还是很重的

由于V代码编译后生成的是C代码,然后再调用C编译器编译成可执行文件

这样的机制决定了V语言可以很方便地调用C世界的各种代码库

这对于V语言来说,是个很大的一个优势,毕竟C代码库经过多年的积累,很丰富

如何调用

在V代码里调用C代码也非常简单,在V标准库里随处可见

以下是调用C代码库的基本步骤:

  1. 定义#flag

    定义flag这一步是可选的,比如调用C标准库的函数就不需要

    一般调用第三方库才需要用到,如果有需要,flag的定义要放在C语言宏之前

    关于#flag详细内容,参考下面的flag段落

  2. 在V代码中使用C语言宏

    比如#include宏或#define宏,编译时这些宏会被原封不动地搬到生成的C代码中

  3. 然后用V的语法定义要使用的C函数或C结构体的声明

    函数名或结构体名一定要在C名称的基础上添加C.前缀

  4. 然后就可以在V代码中使用C函数或结构体

    调用的时候,名称前可以使用C.作为前缀,也可以不使用,不过还是建议统一使用C.前缀,代码更容易区分是调用C函数或C结构体.

    其实V编译的时候,会统一把函数和结构体名称前面的C.统一去掉,这样在C代码里面就可以正常调用了

调用标准库的例子:

  1. module main
  2. #include <stdio.h> //使用C语言宏,包含头文件
  3. fn C.getpid() int //定义要使用的C函数声明,名字在C函数名字前统一加上C.前缀
  4. fn main(){
  5. pid:=C.getpid()
  6. pid2:=getpid() //C.前缀去掉也可以
  7. println(pid)
  8. println(pid2)
  9. }

myslq库中的参考代码:

  1. //宏
  2. #flag -lmysqlclient
  3. #flag linux -I/usr/include/mysql
  4. #include <mysql.h>
  5. //声明要使用的C结构体
  6. struct C.MYSQL
  7. struct C.MYSQL_RES
  8. //声明要使用的C函数
  9. fn C.mysql_init(mysql &C.MYSQL) &C.MYSQL
  10. fn C.mysql_real_connect(mysql &C.MYSQL, host &byte, user &byte, passwd &byte, db &byte, port u32, unix_socket &byte, clientflag u64) &C.MYSQL
  11. fn C.mysql_query(mysql &C.MYSQL, q &byte) int
  12. fn C.mysql_select_db(mysql &C.MYSQL, db &byte) int
  13. fn C.mysql_error(mysql &C.MYSQL) &byte
  14. fn C.mysql_errno(mysql &C.MYSQL) int
  15. fn C.mysql_num_fields(res &C.MYSQL_RES) int
  16. fn C.mysql_store_result(mysql &C.MYSQL) &C.MYSQL_RES
  17. fn C.mysql_fetch_row(res &C.MYSQL_RES) &&byte
  18. fn C.mysql_free_result(res &C.MYSQL_RES)
  19. fn C.mysql_real_escape_string_quote(mysql &C.MYSQL, to &byte, from &byte, len u64, quote byte) u64
  20. fn C.mysql_close(sock &C.MYSQL)
  21. //调用C函数或结构体
  22. pub fn connect(server, user, passwd, dbname string) ?DB {
  23. conn := C.mysql_init(0)
  24. if isnil(conn) {
  25. return error_with_code(get_error_msg(conn), get_errno(conn))
  26. }
  27. conn2 := C.mysql_real_connect(conn, server.str, user.str, passwd.str, dbname.str, 0, 0, 0)
  28. if isnil(conn2) {
  29. return error_with_code(get_error_msg(conn), get_errno(conn))
  30. }
  31. return DB {conn: conn2}
  32. }

另一个集成C代码库的例子:vlib/clipboard/clipboard_linux.c.v

使用了结构体注解[typedef]来定义C语言的结构体

  1. //定义C宏
  2. #flag -lX11
  3. #include <X11/Xlib.h>
  4. //定义C结构体
  5. [typedef]
  6. struct C.Display //在v代码中只需使用C结构体,不使用结构体字段
  7. [typedef]
  8. struct C.XSelectionRequestEvent{ //在v代码中要使用结构体字段,要定义所需的字段
  9. mut:
  10. display &C.Display /* Display the event was read from */
  11. owner C.Window
  12. requestor C.Window
  13. selection C.Atom
  14. target C.Atom
  15. property C.Atom
  16. time int
  17. }
  18. //定义C函数
  19. fn C.XInitThreads() int
  20. fn C.XCloseDisplay(d &Display)
  21. fn C.XFlush(d &Display)
  22. fn C.XDestroyWindow(d &Display, w C.Window)

使用$env编译时函数

$env也可以在#flay和#include等C宏中使用,让C宏的定义更灵活

  1. module main
  2. //可以在C宏语句中使用,让C宏的定义更灵活
  3. #flag linux -I $env('JAVA_HOME')/include
  4. fn main() {
  5. compile_time_env := $env('PATH')
  6. println(compile_time_env)
  7. }

简单封装

其实,直接在V代码中使用C函数或者结构体也是可以的,不过,由于命名方式不一致的原因,习惯上也可以对C函数或结构体,进行一层简单封装,名字可以重新改为V风格,或者更为简短的名字

一般来说,对一个C代码库中的简单封装会涉及到:结构体,函数,枚举这3大类

如果是小的C代码库,可以直接把这3类的简单封装都放在一个V源文件中

如果C代码库规模大一些,也可以这3类,各自单独一个V源文件,归属于同一个V模块

以下代码是GUI代码库中引用了sokol C代码库后,进行的简单封装:

vlib/sokol/sokol.v部分代码:

  1. //只要在同模块中的任何一个V源文件中引入,该模块的其他源文件就可以直接使用C代码库内容
  2. #define SOKOL_IMPL
  3. #define SOKOL_NO_ENTRY
  4. #include "sokol_app.h"
  5. #define SOKOL_IMPL
  6. #define SOKOL_NO_DEPRECATED
  7. #include "sokol_gfx.h"
  8. //可以直接使用,函数以C.作为前缀
  9. pub fn init_sokol() {
  10. C.sapp_isvalid()
  11. C.sapp_width()
  12. }
  13. //也可以进行简单的封装
  14. module sapp
  15. [inline] //可以给函数添加inline注解,变为内联函数
  16. pub fn isvalid() bool {
  17. return C.sapp_isvalid()
  18. }
  19. [inline]
  20. pub fn width() int {
  21. return C.sapp_width()
  22. }

启用全局变量

默认情况下编译器是没有全局变量声明的,但是为了跟C代码集成,有时候需要定义全局变量,可以在调用编译器时,通过增加 -enable-globals选项来启用

  1. v -enable-globals run main.v
  1. module main
  2. // 单个全局变量定义
  3. __global g1 int
  4. // 组定义全局变量,类似常量的定义
  5. __global (
  6. g2 byte
  7. g3 byte
  8. )
  9. fn main() {
  10. g1 = 1
  11. g2 = 2
  12. g3 = 3
  13. println(g1)
  14. println(g2)
  15. println(g3)
  16. }

函数的[inline]注解

对C函数进行简单的封装时,可以给函数添加inline注解,编译生成C代码时,这个函数就会变成C语言里的static inline函数

内联函数有些类似于宏,内联函数的代码会被直接嵌入在它被调用的地方,调用几次就嵌入几次,没有使用call指令。这样省去了函数调用时的一些额外开销,不过调用次数多的话,会使可执行文件变大,这样会降低整个程序的运行速度

像上面那个简单的封装,函数只有1行代码,嵌入到被调用的地方也还是1还代码,既能省去函数调用时的额外开销,提升性能,又不会使可执行文件变大

同时也可以统一和简化C函数的命名,变为V风格的简短命名,一举多得

  1. [inline]
  2. pub fn width() int {
  3. return C.sapp_width()
  4. }

结构体简单封装

定义C结构体等价的V版本结构体,V版本结构体名称以C.做前缀

这样就可以直接使用V版本的结构体来创建变量

用V版本的结构体创建变量,编译生成的C代码中,就是用C版本结构体创建了变量

C代码库中的结构体:

  1. typedef struct sapp_event {
  2. uint64_t frame_count;
  3. sapp_event_type type;
  4. sapp_keycode key_code;
  5. uint32_t char_code;
  6. bool key_repeat;
  7. uint32_t modifiers;
  8. sapp_mousebutton mouse_button;
  9. float mouse_x;
  10. float mouse_y;
  11. float scroll_x;
  12. float scroll_y;
  13. int num_touches;
  14. sapp_touchpoint touches[SAPP_MAX_TOUCHPOINTS];
  15. int window_width;
  16. int window_height;
  17. int framebuffer_width;
  18. int framebuffer_height;
  19. } sapp_event;

定义等价的V版本结构体:字段数量一样,字段名称一样,字段类型一样

如果字段类型是枚举的,也可以再定义等价的V版本枚举

  1. pub struct C.sapp_event {
  2. pub:
  3. frame_count u64
  4. @type EventType //这个type字段有点特殊,因为是V的关键字,要用@开头才可以
  5. key_code KeyCode //枚举类型
  6. char_code u32
  7. key_repeat bool
  8. modifiers u32
  9. mouse_button MouseButton //枚举类型,下面有MouseButton的V版本定义
  10. mouse_x f32
  11. mouse_y f32
  12. scroll_x f32
  13. scroll_y f32
  14. num_touches int
  15. touches [8]sapp_touchpoint //数组类型
  16. window_width int
  17. window_height int
  18. framebuffer_width int
  19. framebuffer_height int
  20. }

枚举简单封装

其实就是定义一个跟C版本枚举一样的枚举,枚举名和枚举值一样

  1. pub enum MouseButton {
  2. invalid = -1
  3. left = 0
  4. right = 1
  5. middle = 2
  6. }

关于#flag标记

这个#flag标记跟v编译器的-cflags选项的用法是一样的,用于传递额外的flag参数给C编译器

关于C编译器的flag参数可以参考:https://colobu.com/2018/08/28/15-Most-Frequently-Used-GCC-Compiler-Command-Line-Options/

  1. 要在使用C宏之前先定义#flag

  2. -l 表示在库文件的搜索路径列表中添加指定的路径

    -I 表示在头文件的搜索路径中添加指定的路径

    -D 表示设置编译时变量

  3. 还可以在#flag后增加平台标识,针对不同的平台配置不同的flag,目前支持的平台有:linux,darwin,windows

以下例子,提供参考:

  1. //#flag文档里面的:
  2. #flag linux -lsdl2
  3. #flag linux -Ivig
  4. #flag linux -DCIMGUI_DEFINE_ENUMS_AND_STRUCTS=1
  5. #flag linux -DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1
  6. #flag linux -DIMGUI_IMPL_API=
  1. //mysql包里面的:
  2. #flag -lmysqlclient //-l开头,表示在库文件的搜索路径中添加mysqlclient
  3. #flag linux -I /usr/include/mysql //-I开头,在头文件的搜索路径中添加指定的路径, 针对linux平台,这样才可以搜索到下面的mysql.h头文件
  4. #include <mysql.h>
  1. //sokol包里面的:
  2. #flag -I @VROOT/thirdparty/sokol //@VROOT指向v编译器的根路径
  3. #flag -I @VROOT/thirdparty/sokol/util
  4. #flag darwin -fobjc-arc //针对mac平台,提供额外的flag参数:-fobjc-arc
  5. #flag linux -lX11 -lGL //针对linux平台,提供额外的flag参数
  6. #flag windows -lgdi32 //针对window平台,提供额外的flag参数:-l开头,表示链接gdi32
  7. // OPENGL
  8. #flag -DSOKOL_GLCORE33 //提供额外的flag参数:-DSOKOL_GLCORE33
  9. #flag darwin -framework OpenGL -framework Cocoa -framework QuartzCore