在上一篇關於FFI的文章中,我嘗試用FFI去呼叫raylib,但是FFI功能還不完善,涉及巢狀結構體、結構體裡有陣列的功能都不能使用,所以我又實現了一個PHP擴充套件:raylib-phpcpp。
雖然只是業餘專案,我還是想做得儘量完善,所以定了目標:
- 完整支援:主要功能不能缺(目前覆蓋88%的raylib函式,算上PHP有替代品的,覆蓋92%)
- 容易維護:raylib新版本釋出時可以儘快跟進
- 快速開發:時間不能投入太多,畢竟是業餘專案
下面會看到,我主要通過程式碼生成的方式實現上面這些目標。
開發一個PHP擴充套件,據我所知至少有4種方式:C、Zephir、PHP-X、PHP-CPP。
C的開發難度太高,我也不熟悉Zend API,所以不考慮。
Zephir是類似PHP的語言,可以被編譯成C。Zephir可以通過優化器和CBLOCK呼叫C程式碼,但是這兩種方法還是要用到Zend API,而且沒有簡單的方法去封裝一個C的結構體,所以Zephir被排除掉。
PHP-X和PHP-CPP有點像,都是C++封裝了Zend API,可以用C++實現擴充套件,PHP-CPP功能相對完善,示例也多,所以我最後選擇了PHP-CPP。
我只是看了一下文件和StackOverflow,沒有深入調研每個方案,所以以上資訊很可能不準確。
首先raylib的每一個結構體,我都使用一個類封裝,這些類裡都有一個data
屬性,儲存對應的結構體,例如Vector3
是:
class Vector3 : public Php::Base {
public:
::Vector3 data;
Vector3(::Vector3 x) { data = x; } // 從raylib的Vector3構造Vector3
// ... getter and setter
};
在呼叫raylib函式的時候,我可以直接取出data
屬性傳過去就可以了,返回結果也類似,直接建構函式儲存到data
裡。
有了類還不夠,每個結構體都有很多欄位,例如Vector3
有x
、y
、z
,這些欄位的會通過__ get
和__set
方法返回,例如x
欄位的getter、setter:
Php::Value getx() {
double result = data.x;
return result;
}
void setz(const Php::Value &v) { data.z = (double)v; }
然後註冊到Vector3
的裡:
rlVector3.property("x", &Vector3::getx, &Vector3::setx);
對於巢狀的結構體,實現也類似的,例如Camera3D
裡有一個Vector3型別的position
欄位:
Php::Value getposition() {
Php::Value result =
Php::Object("RayLib\\Vector3", new Vector3(data.position));
return result;
}
void setposition(const Php::Value &v) {
data.position = ((Vector3 *)(v.implementation()))->data;
}
rlCamera3D.property("position", &Camera3D::getposition, &Camera3D::setposition);
最終效果就是可以在PHP程式碼裡直接構造和訪問結構體的欄位:
$vec = RL::Vector3(1, 2, 3);
$vec->x = $vec->y + 1;
$camera = RL::Camera2D(RL::Vector2(1, 1), RL::Vector2(0, 0), 1.0, 1.0);
echo $camera->target->x;
對raylib裡的每一個函式,我生成一個對應的方法,這個方法接收一個Php::Value
陣列作為引數,陣列裡的每一項會被轉換或者提取出data
,然後傳給raylib的函式。
例如DrawPixel:
static void DrawPixel(Php::Parameters ¶ms) {
int p0 = params[0]; // 標量直接取出
int p1 = params[1];
::Color p2 = ((Color *)(params[2].implementation()))->data; // 結構體提取出data
::DrawPixel(p0, p1, p2); // 呼叫raylib的DrawPixel
}
這些方法都是RL
類的靜態方法,會被註冊到PHP對應的方法裡:
rlClass.method<&RL::DrawPixel>("DrawPixel");
有些函式需要使用結構體指標,只需在提取data
後取址即可:
static void UpdateCamera(Php::Parameters ¶ms) {
::Camera3D *p0 = &((Camera3D *)(params[0].implementation()))->data;
::UpdateCamera(p0);
}
經過上面兩節的分析,可以發現這裡面有明顯的模式,把全部繫結寫出來只是體力活。對於一個業餘專案,投入這麼多時間去做重複工作實在不值和不爽,所以我直接從raylib的標頭檔案直接生成程式碼。
生成程式碼的第一步是解析raylib.h
,這部分工作可以直接使用libclang完成,我用了rust的繫結,使用非常方便:
let clang = Clang::new().unwrap();
let index = Index::new(&clang, false, false);
let tu = index.parser("raylib.h").parse().unwrap();
let mut functions: Vec<FunctionDecl> = Vec::new();
let mut structs: Vec<StructDecl> = Vec::new();
// 遍歷每個entity,收集函式和結構體宣告:
for e in tu.get_entity().get_children().into_iter() {
if e.get_kind() == EntityKind::FunctionDecl {
functions.push(FunctionDecl{entity: e});
}
if e.get_kind() == EntityKind::StructDecl {
structs.push(StructDecl{entity: e});
}
}
接著就是遍歷每一個函式和結構體,按照型別資訊生成程式碼,最終結果是生成了4千行的C++程式碼: raylib.cpp。
用程式碼生成的方式,可以很好地完成定下的目標,大部分功能都直接支援,新版本釋出時只需重新生成程式碼,而且工作量比直接手寫少了很多(生成器5百行,生成程式碼4千行),而且非常容易統一新增功能和修改實現。例如我要新增引數型別檢查,可以統一新增,例如改為全域性函式,也只需修改方法的註冊方式。
- 有些函式需要使用
void *
傳陣列引數或返回資料,可以暴露成一個PHP的字串,但是使用者使用不太友好,設想需要一個Typed Array型別,目前沒實現 - raylib的字串處理函式沒有繫結,不過PHP自帶的字串函式更完善,直接用PHP就好
下一步就是繼續完善程式碼生成器和做一個小遊戲啦。