Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate function bind #2

Merged
merged 18 commits into from
Dec 29, 2023
12 changes: 11 additions & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,17 @@ jobs:
- run: rustup show
- run: rustup component list --installed
- run: cargo --version
- run: cargo build -vv

# Enable cc.
- name: Display PATH
run: echo $Env:PATH
- run: copy "C:\mingw64\bin\gcc.exe" "C:\mingw64\bin\cc.exe"
- run: dir "C:\mingw64\bin"
- run: gcc --version
- run: cc --version

- run: cargo install cargo-make
- run: cargo make build-plain -vv
- run: cargo test _ci -- --nocapture
- run: rustup component add clippy
- run: cargo clippy
12 changes: 12 additions & 0 deletions Makefile.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[tasks.build-plain-sys]
workspace = false
command = "cargo"
args = ["build"]
cwd = "raylib-rs-plain-sys"

[tasks.build-plain]
workspace = false
command = "cargo"
args = ["build"]
cwd = "raylib-rs-plain"
dependencies = ["build-plain-sys"]
16 changes: 16 additions & 0 deletions raylib-rs-plain-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ fn clone_raylib() {
.arg(RAYLIB_REPOSITORY_PATH)
.status()
.unwrap();

std::process::Command::new("make")
.current_dir(RAYLIB_REPOSITORY_PATH.to_owned() + "/parser")
.status()
.unwrap();

std::process::Command::new(RAYLIB_REPOSITORY_PATH.to_owned() + "/parser" + "/raylib_parser")
.args([
"--output",
"output/raylib_api.json",
"--format",
"JSON"
])
.current_dir(RAYLIB_REPOSITORY_PATH.to_owned() + "/parser")
.status()
.unwrap();
}

fn build_raylib() {
Expand Down
2 changes: 1 addition & 1 deletion raylib-rs-plain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ raylib-rs-plain-common = { version = "0.1.0", path = "../raylib-rs-plain-common"
serde = { version = "1.0.164", features = ["derive"] }
serde_json = "1.0.96"
regex = "1.8.4"

convert_case = "0.6.0"
237 changes: 217 additions & 20 deletions raylib-rs-plain/build.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
use serde::{Deserialize, Serialize};
use std::{fs, env, path::Path, time::Duration, thread};
use std::{fs, env};
use raylib_rs_plain_common as rl_common;
use regex::Regex;
use std::process::Command;
use convert_case::{Case, Casing};

fn main() {
generate_define();
let raylib_api_json_path = "../raylib-rs-plain-sys/".to_owned() + rl_common::RAYLIB_REPOSITORY_PATH + "/parser/output/raylib_api.json";
let content = fs::read_to_string(raylib_api_json_path).unwrap();
let raylib_api:RaylibApi = serde_json::from_str(&content).unwrap();

generate_function(&raylib_api);
generate_define(&raylib_api);
}

#[derive(Serialize, Deserialize)]
struct RaylibApi {
defines: Vec<Identifier>,
functions: Vec<FunctionIdentifier>,
structs: Vec<StructIdentifier>,
aliases: Vec<AliaseIdentifier>,
callbacks: Vec<CallbackIdentifier>,
}

#[derive(Serialize, Deserialize)]
Expand All @@ -20,36 +31,222 @@ struct Identifier {
value:serde_json::Value,
}

fn generate_define() {
let raylib_api_json_path = "../raylib-rs-plain-sys/".to_owned() + rl_common::RAYLIB_REPOSITORY_PATH + "/parser/output/raylib_api.json";
#[derive(Serialize, Deserialize)]
struct FunctionIdentifier {
name:String,
description:String,
#[serde(rename = "returnType")]
return_type:String, // todo I want to parse this at a high level to enum, not string.
params: Option<Vec<ArgIdentifier>>,
}

#[derive(Serialize, Deserialize)]
struct ArgIdentifier {
#[serde(rename = "type")]
arg_type:String,
name:String,
}

#[derive(Serialize, Deserialize)]
struct StructIdentifier {
name:String,
}

#[derive(Serialize, Deserialize)]
struct AliaseIdentifier {
name:String,
}

#[derive(Serialize, Deserialize)]
struct CallbackIdentifier {
name:String,
}

fn generate_function(raylib_api:&RaylibApi) {
let mut raylib_function = String::new();
let pkg_name = env::var("CARGO_PKG_NAME").unwrap();
raylib_function.push_str(&("/* automatically generated by ".to_owned() + &pkg_name + " */\n"));
raylib_function.push_str("#![allow(clippy::not_unsafe_ptr_arg_deref, clippy::too_many_arguments)]\n");
raylib_function.push_str(generate_header(raylib_api).join("\n").as_str());

for identifier in &raylib_api.functions {
let comment = format!(
"/** {} */\n",
identifier.description,
);
let return_type = c_to_rs_return_type(identifier.return_type.as_str());
let body = format!(
"pub fn {}({}){} {{ {} }}\n",
identifier.name.to_case(Case::Snake),
generate_arg(&identifier.params),
return_type,
generate_function_body(identifier, return_type.as_str()),
);
raylib_function.push_str(&(comment + &body + "\n"));
}

fs::write("./src/function.rs", raylib_function).unwrap();
Command::new("rustfmt")
.arg("src/function.rs")
.status().unwrap();
}

// Sometimes raylib-rs-plain-sys build is not completed and raylib_api.json cannot be obtained.
// - Therefore, wait until raylib_api.json can be obtained.
let artifact_path = Path::new(&raylib_api_json_path);
let max_attempts = 20;
let wait_duration = Duration::from_secs(5);
for attempt in 1..=max_attempts {
if artifact_path.exists() {
println!("Artifact found. Continue with build.");
break;
} else if attempt == max_attempts {
panic!("Artifact not found.");
fn generate_arg(_params:&Option<Vec<ArgIdentifier>>) -> String {
if _params.is_none() {
return "".to_string();
}
let params = _params.as_ref().unwrap();

let rs_params:Vec<String> = params.iter().filter_map(
|param|
// Variable length arguments not supported in rust
if param.name == "args" && param.arg_type == "..." {
Option::None
} else {
println!("Retry because artifact not found.({}/{})", attempt, max_attempts);
thread::sleep(wait_duration);
Option::Some(
fix_reserved_keyword(param.name.to_case(Case::Snake).as_str()) + ":" + c_to_rs_type(param.arg_type.as_str()).as_str()
)
}
).collect();
return rs_params.join(", ");
}

fn fix_reserved_keyword(name:&str) -> String {
return match name {
"box" => "box_".to_owned(),
"type" => "type_".to_owned(),
_ => name.to_owned(),
}
}

let content = fs::read_to_string(raylib_api_json_path).unwrap();
fn generate_function_body(function:&FunctionIdentifier, return_type:&str) -> String {
let mut body = String::new();
if !return_type.is_empty() {
body += "return ";
};

let raylib_api:RaylibApi = serde_json::from_str(&content).unwrap();
let arg:String = match &function.params {
Option::None => "".to_string(),
Option::Some(params) => {
let rs_params:Vec<String> = params.iter().filter_map(
|param|
// Variable length arguments not supported in rust
if param.name == "args" && param.arg_type == "..." {
Option::None
} else {
Option::Some(fix_reserved_keyword(param.name.to_case(Case::Snake).as_str()))
}
).collect();
rs_params.join(", ")
},
};

body += ("unsafe { rl::".to_owned()
+ function.name.as_str()
+ "(" + arg.as_str() + ") };"
).as_str();
return body;
}

fn generate_header(raylib_api:&RaylibApi) -> Vec<String> {
let mut header:Vec<String> = vec![
"use raylib_rs_plain_sys as rl;".to_string(),
// "use std::ffi::CString;".to_string(),
"use ::std::os::raw::c_int;".to_string(),
"use ::std::os::raw::c_uint;".to_string(),
"use ::std::os::raw::c_long;".to_string(),
"use ::std::os::raw::c_void;".to_string(),
"use ::std::os::raw::c_uchar;".to_string(),
"use ::std::os::raw::c_char;".to_string(),
"\n".to_string(),
];

let struct_use:Vec<String> = raylib_api.structs.iter().map(
|identifier|
"pub use rl::".to_owned() + &identifier.name + ";"
).collect();
header.extend(struct_use);

let aliases:Vec<String> = raylib_api.aliases.iter().map(
|identifier|
"pub use rl::".to_owned() + &identifier.name + ";"
).collect();
header.extend(aliases);

let callbacks:Vec<String> = raylib_api.callbacks.iter().map(
|identifier|
"pub use rl::".to_owned() + &identifier.name + ";"
).collect();
header.extend(callbacks);

header.push("\n".to_string());
return header;
}

fn c_to_rs_type(c_type:&str) -> String {
let mut modifier:String = String::new();
let mut unprocessed_elements:Vec<&str> = Vec::new();
for type_element in c_type.split(' ') {
let mut asterisk_part:String = String::new();
for asterisk in type_element.chars() {
if asterisk != '*' {
continue;
}
// First asterisk if there are multiple asterisks.
if type_element.len() > 1 && !asterisk_part.contains('*') {
asterisk_part += "*mut ";
continue;
}
if c_type.contains("const") {
asterisk_part += "*";
} else {
asterisk_part += "*mut ";
}
}
if !asterisk_part.is_empty() {
modifier = asterisk_part + modifier.as_str();
continue;
}

if type_element == "const" {
modifier += (type_element.to_owned() + " ").as_str();
continue;
}

unprocessed_elements.push(type_element);
}

let c_type_main = unprocessed_elements.join(" ");
let rust_type = match c_type_main.as_str() {
"unsigned char" => "c_uchar",
"unsigned int" => "c_uint",
"int" => "c_int",
"long" => "c_long",
"float" => "f32",
"double" => "f64",
"void" => "c_void",
"char" => "c_char",
_ => c_type_main.as_str(),
};
return modifier + rust_type;
}

fn c_to_rs_return_type(c_type:&str) -> String {
if c_type == "void" {
return "".to_owned();
}

return " -> ".to_owned() + c_to_rs_type(c_type).as_str();
}

fn generate_define(raylib_api:&RaylibApi) {
let mut raylib_define = String::new();
let pkg_name = env::var("CARGO_PKG_NAME").unwrap();
raylib_define.push_str(&("/* automatically generated by ".to_owned() + &pkg_name + " */\n"));
raylib_define.push_str("use raylib_rs_plain_sys as rl;\n");
raylib_define.push_str("pub use rl::Color;\n");
let reg = Regex::new(r"[^0-9,]").unwrap();
for identifier in raylib_api.defines {
for identifier in &raylib_api.defines {
if identifier.type_item == "COLOR" {
println!("{}", identifier.value);
let color_value = identifier.value.to_string();
Expand Down
Loading