绑定c到d

原地址
作者:(m.p)
我曾经维护一个Derelict(org)项目,主要是一堆函数/类型声明.使我得到绑定c到d的经验,还有个deimos项目.本文探讨一些问题.
首先要考虑静态/动态绑定.静态编译时连接c库/目标文件.动态则指运行时加载dll/so/dylib等.我搞的多是动态,而deimos则大多数是静态.注意区分静态绑定静态链接.静态绑定,可编译时与动态库,静态库连接.c++世界里,编译时链接动态库很常见.窗口中通过链接库.lib调用库.dll.Posix上也可以.所以d静态绑定.你可以编译时连接库.lib(也可导入库,dll),库.a/库.so(动态链接),而动态绑定,则是在运行时手动链接动态库.
动态库,一般用于插件或应用子系统的热切换.如opengl/Direct3D渲染器的切换.只要按指针声明导出共享库符号,调用系统加载库api,然后提取出导出符号赋值给指针.这就是动态绑定.他牺牲了让操作系统决定加载什么及何时加载的方便性,总之,静态绑定编译时可与静态库/动态库链接.而动态绑定运行时加载动态库有关.
d理解cabi,所以可与c目标文件/库链接,Posix系统没问题.用的是gcc.在窗口上,有omf/coff格式问题.GDC与LDC都支持coff,而dmd,32位用Optlink.64位则生成coff格式.如果忽略格式,可用动态绑定.静态绑定,则要求链接器能识别目标格式.
则要求你用输出omf格式编译器编译c库.或用coffimplib转换工具转为omf,或用工具从dll提取链接.如果公开绑定,是否分发多个格式.还是用户自己取c库.动态库的缺点是,没有静态程序.静态库就是有多个格式,可能会搞混.
然后,决定是手动/自动.还是要自动,手动太多,太慢.swig,htod没试过.不过必须得了解c与d间如何转换.
同c对接必读文章
先要了解目标c库调用约定.大部分跨平台c库cdecl调用约定,而窗口stdcall调用约定.有些库在窗口用stdcall,而其他平台用c调用约定.d存储类干两件事:1,给定函数不在当前模块.2,通过链接属性指定调用约定.
d属性文档列举支持的属性,但对c来说,主要是C, Windows和System三种.静态/动态绑定的链接属性形式不一样.静态绑定函数声明,动态绑定函数指针.extern(C)用于c调用约定.未指定,也默认为c调用约定.有些编译器允许命令行修改调用约定.

// In C
extern void someCFunction(void);
// In D
extern( C ) void someCFunction();

extern(Windows),则表明stdcall调用约定.在c头上类似__stdcall前缀.在窗口上,则是类似WINAPI, APIENTRY,和PASCAL的东西.

// In C
#define WINAPI __stdcall
extern WINAPI void someWin32Function(void);

// In D
extern( Windows ) void someWin32Function();

extern(System)在绑定OpenGL等库时有用.在窗口中用stdcall,在其他系统用cdecl.

// In C
#ifdef _WIN32
#include 
#define MYAPI WINAPI
#else
#define MYAPI
#endif
extern MYAPI void someFunc(void);
// In D
extern(System) void someFunc();
//跟随系统走

实践中有许多技术来添加调用约定,要仔细检查.在d中,可以统一起来,而不是挨个加:

//属性块
extern( C )
{
	void functionOne();
	double functionTwo();
}

//或这样
extern( C ):
	void functionOne();
	void functionTwo();

d曾经有Typedefs,其创建新类型,而不是别名.d别名,则不创建新类型,只是另外一个名字.typedef已过时,除了在中,别名ctypedefd的等价物.c中有大量typedef.

typedef int foo_t;
typedef float bar_t;

d接口中最好保留原型名.d接口要尽量兼容c接口.这样,示例啊,其他代码啊都可以容易的移植过来.
第一件事是如何将整/浮转至d.对接细节文档这样:

alias int foo_t;
alias float bar_t;

注意长/正长.

// C头
typedef long mylong_t;
typedef unsigned long myulong_t;

//D模块
import core.stdc.config;

//导入是私,但别名是公,外部可见
alias c_long mylong_t;
alias c_ulong myulong_t;

长正长见入门
翻译c的stdint.h时,两种方法:

// From SDL_stdinc.h
typedef int8_t Sint8;
typedef uint8_t Uint8;
typedef int16_t Sint16;
typedef uint16_t Uint16;
...

// In D, 不用core.stdc.stdint
alias byte Sint8;
alias ubyte Uint8;
alias short Sint16;
alias ushort Uint16;
...

//用导入
import core.stdc.stdint;

alias int8_t Sint8;
alias uint8_t Uint8;
alias int16_t Sint16;
alias uint16_t Uint16;
...

本地类型,长度固定,很直接.用core.stdc.stdint,复制了一份c头,用别名替换typedef.
转换枚举只需要复制/粘贴.

// In C
enum
{
	ME_FOO,
	ME_BAR,
	ME_BAZ
};

// In D
enum
{
	ME_FOO,
	ME_BAR,
	ME_BAZ,
}

注意d中无分号,且可有逗号.命名枚举,要加前缀.

// In C
typedef enum
{
	ME_FOO,
	ME_BAR,
	ME_BAZ
} MyEnum;

// In D
enum MyEnum
{
	ME_FOO,
	ME_BAR,
	ME_BAZ
}

//函数中
MyEnum me = MyEnum.ME_FOO;//这里

主要为了类型安全,为了方便及兼容:

alias MyEnum.ME_FOO ME_FOO;
alias MyEnum.ME_BAR ME_BAR;
alias MyEnum.ME_BAZ ME_BAZ;
//加上别名
MyEnum me = ME_FOO;

但上面太冗余,如果安全不重要,可以这样:

alias int MyEnum;
enum
{
	ME_FOO,
	ME_BAR,
	ME_BAZ
}

这与c的行为一样了.
#define,c中用来定义常量.而d中枚举,不仅可表示传统枚,也可表示清单常量,就不必用不变限定符了.清单常量只含一个成员枚举量,因而不用加括号.

//浮型清单常量
enum float Foo = 1.003f;

//用动引用来声明,枚能够自动推导类型.
enum Foo = 1.003f; // float
enum Bar = 1.003; // double
enum Baz = "Baz!" // string
//如下示例:
//C端
#define FOO_SOME_NUMBER 100
#define FOO_A_RELATED_NUMBER 200
#define FOO_ANOTHER_RELATED_NUMBER 201

//D端
enum FOO_SOME_NUMBER = 100
enum FOO_A_RELATED_NUMBER = 200
enum FOO_ANOTHER_NUMBER = 201

// 归为一组
enum
{
	FOO_SOME_NUMBER = 100,
	FOO_A_RELATED_NUMBER = 200,
	FOO_ANOTHER_NUMBER = 201,
}

多的话,归为一组,1/2个的话,单独搞.还可以多个串

// In C
#define LIBNAME "c库"
#define AUTHOR "XX"
#define COMPANY "哈哈"

//D中,都可加入枚串中
enum : string
{
	LIBNAME = "c库",
	AUTHOR = "XX",
	COMPANY = "哈哈",
}

当然,枚:(继承)其他也是可以的.注意尾逗号.
,大部分c构,可稍微/甚至不需要修改.直接改为d,

// In C
struct foo_s
{
	int x, y;
};

typedef struct
{
	float x;
	float y;
} bar_t;

// In D
struct foo_s
{
	int x, y;
}
//无分号.
struct bar_t
{
	float x;
	float y;
}

主要就是typedef.有时有两个结构名字.

// In C
typedef struct foo_s
{
	int x;
	struct foo_s *next;
} foo_t;
//一个_s,_t.

// In D
struct foo_t//用后面那个.
{
	int x;
	foo_t *next;
}

还有c的不透明结构/c++的前向引用,

// In C
typedef struct foo_s foo_t;

// In D
struct foo_t;

翻译结构成员,也是差不多,Typedefs, Aliases,和本地类型,但还有些陷阱.命名函数/类型时,尽量与c一样.但有时c中名字有d关键字.因而一般在前面加个_.然后在文档中说明.

// In C
typedef struct
{
	//d关键字.
	int module;
} foo_t;

// In D
struct foo_t
{
	int _module;//加上_.
}

还有就是一些c库成员,包装在#define块中.在绑定及用c库时,易出错,转d容易,但用时要小心.

// In C
typedef struct
{
	float x;
	float y;
	#ifdef MYLIB_GO_3D
	float z;
	#endif
} foo_t;

// In D
struct foo_t
{
	float x;
	float y;
	//用版本限定块,与环境相关的名字
	version(Go3D) float z;
}

编译时加上,-version=Go3D开关.如果绑定是,则应用程序也要加.这就折腾了.c库也要这样编译.如果公开,则还要加上版本文档.真是坑人.
还有个坑,就是位域,一般用std.bitmanip库来解决,但不是特效药,因为c标准,未定义位域的顺序.

typedef struct
{
	int x : 2;
	int y : 4;
	int z: 8;
} foo_t;

不保证字段的顺序,以及是否及在哪填空白.不同编译器,不同平台都不一样.必须得手动匹配.可以考虑用std.bitmanip.bitfields

// D用std.bitmanip.bitfields
struct foo_t
{
	mixin(bitfields!(//来转换
		int, "x", 2,
		int, "y", 4,
		int, "z", 8,
		int, "", 2)); // padding
}

必须为8的倍数,上面是2个空位.从最不重要位开始.必须要与c编译器匹配.
其余是用

struct foo_t
{
	int flags;
	int x() @property { ... }
	int y() @property { ... }
	int z() @property { ... }
}

方法.但问题是...里面放什么,取决于c编译器,从最不重要位还是最重要位开始.及字段间是否有空白间隙.与std.bitmanip.bitfields遇着的问题一样.
我遇见过,然后用单个不能访问它的标志位字段,如果在多平台同其他编译器的库一起运行,没有简单解决方法.只有交给用户了.特定绑定,针对特定编译器/特定平台.
函数指针.一般作为回调.d有自己的语法,所以必须适配.

int function() MyFuncPtr;
//d风格指针声明

格式为中类型->函数关键字->参数列表->函数指针名
直接用MyFuncPtr也可以,但也可定义别名.

alias int function() da_MyFuncPtr;
da_MyFuncPtr MyFuncPtr;
//没啥意思
int foo(int i)
{
	return i;
}
 
void main()
{
	int function(int) fooPtr;//
	fooPtr = &foo;
	alias int function(int) da_fooPtr;
	da_fooPtr fooPtr2 = &foo;
	import std.stdio;
	writeln(fooPtr(1));
	writeln(fooPtr2(2));
}
//这样转换
// In C, foo.h
typedef int (*MyCallback)(void);
// In D
extern( C ) alias int function() MyCallback;

用别名,这样,你可像c一样用.

// In C, foo.h
extern void foo(int (*BarPtr)(int));
 
// In D.
// 1这样
extern( C ) void foo(int function(int) BarPtr);
 
// 2这样
extern( C ) alias int function(int) BarPtr;
extern( C ) void foo(BarPtr);

2较好,可以复用.接下来,构中内联声明函数指针.

// In C, foo.h
typedef struct
{
	int (*BarPtr)(int);
} baz_t;

// In D
struct baz_t
{
	extern( C ) int function(int) BarPtr;   
}

静态绑定中的函数声明.d中不必声明.实现即是声明.也与你实现/声明位置无关.为链接c库,不必也无权访问实现,因而绑定.为了调用他们,d要知道存在他们.以便链接时找到正确地址.因此,必须声明.

// In C, foo.h
extern int foo(float f);
extern void bar(void);
// In D
extern( C )
{
    int foo(float);
    void bar();
}

动态绑定.用函数指针而不是函数声明.简单声明是不行的.先要考虑初化函数指针.

// D中.
int foo() { return 1; }
void* getPtr() { return cast(void*) &foo; }
void main()
{
    int function() fooPtr;
    fooPtr = getPtr();
}
//编译得到
fptr.d(10): Error: 不能隐式把(getPtr())的(void*)类型转为`int function()`

操作系统用GetProcAddress/dlsym返回空*指针,一种是.

fooPtr = cast(fooPtr)getPtr();//变量,用作类型.

然后这样:

alias int function() da_fooPtr;
da_fooPtr fooPtr = cast(da_fooPtr)getPtr();

所以,用别名要好点.da_表示d别名.还可以:

int foo() 
{ 
	return 1; 
}

void* getPtr() 
{ 
	return cast(void*) &foo; 
}

void bindFunc(void** func) 
{ 
	*func = getPtr(); 
}

void main()
{
	int function() fooPtr;
	bindFunc(cast(void**)&fooPtr);
}//将foo**转为(空**)

第2种,消除了别名.dmd以前未提供栈跟踪.但dmd在编译2进制时,不论是共享库/exe,配置文件都预先配置为导出所有符号.否则,无法实现跟踪栈.但却使我的函数指针与导出库的函数指针冲突了.即使是手动加载共享库.因而只好用回别名方式.不会导出别名函数指针/变量.如果你制作动态绑定,一定要注意这点.我仍然用空**来加载函数指针,因为它更简单.

foo = cast(da_Foo)getSymbol("foo");
//你看不到
//你看到了
foo = bindFunc(cast(void**)&foo, "foo");

手动搞,后者复制粘贴更快.还有一点.给定一个动态绑定函数指针,要遵守d存储变量规则.d变量,默认为线程本地的,即每个线程有份变量拷贝.
如果在一个线程加载,而在另一个线程中调用,会崩溃的.
幸好,d的函数指针默认初化为null,解决方式是跨线程共享,用shared/__gshared.d的一个目标就是使并行更容易.通过共享,可以跨线程共用.编译器会说,这不是线程安全访问方式,与不变,常一样,共享也是传递性的,你引用一个共享变量,这个引用也是共享的了.用__gshared时,就是全局变量,由程序员负责同步.因此,实现动态绑定时,要决定用线本?共享?全局共享?.
一般用__gshared,因为线程都要访问.这时,要确保其自身生命期比访问者更长,因而一般在静态模块构造器和析构器加载/卸载他们.

extern( C )
{
	alias void function(int) da_foo;
	alias int function() da_bar;
}
 
__gshared
{
	da_foo foo;
	da_bar bar;
}

如何加载库.我在DerelictUtil中实现了抽象了加载库并取符号.外部我用自由函数来加载.
绑定要付出努力,动态绑定更累,其他人喜欢静态绑定,但解析c是个难题.我(作者)更喜欢动态绑定.

posted @   zjh6  阅读(5)  评论(0编辑  收藏  举报  
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示