├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── src └── lib.rs └── wrapper.h /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea 3 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hapi-sys" 3 | version = "0.2.0" 4 | license = "MIT" 5 | description = "Raw bindings to SideFx Houdini Engine API" 6 | authors = ["alexxbb "] 7 | repository = "https://github.com/alexxbb/hapi-sys" 8 | edition = "2021" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [build-dependencies] 13 | heck = "0.4.0" 14 | bindgen = "0.60.1" 15 | once_cell = "1.5.2" 16 | 17 | [features] 18 | default = ["rustify"] 19 | rustify = [] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 alexxbb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hapi-sys 2 | Raw Rust bindings to Houdini Engine 19.5 API 3 | 4 | For high-level idiomatic API see [hapi-rs](https://crates.io/crates/hapi-rs) 5 | 6 | [![Cargo](https://img.shields.io/crates/v/hapi-sys.svg)](https://crates.io/crates/hapi-sys) 7 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | use std::env::var; 4 | use std::path::PathBuf; 5 | 6 | use bindgen::callbacks::{EnumVariantValue, ParseCallbacks}; 7 | use once_cell::sync::Lazy; 8 | 9 | #[derive(Debug, Copy, Clone)] 10 | pub enum StripMode { 11 | /// Strip N items at front, e.g N=1: FOO_BAR_ZOO => BAR_ZOO 12 | StripFront(u8), 13 | /// Keeps N items at tail, e.g N=1: FOO_BAR_ZOO => ZOO 14 | KeepTail(u8), 15 | } 16 | 17 | impl StripMode { 18 | pub fn new(m: i32) -> Self { 19 | if m < 0 { 20 | StripMode::KeepTail(m.abs() as u8) 21 | } else { 22 | StripMode::StripFront(m as u8) 23 | } 24 | } 25 | 26 | pub fn strip_long_name<'a>(&self, name: &'a str) -> &'a str { 27 | let mut iter = name.match_indices('_'); 28 | let elem = match self { 29 | StripMode::KeepTail(i) => iter.nth_back((i - 1) as usize), 30 | StripMode::StripFront(i) => iter.nth((i - 1) as usize), 31 | }; 32 | let new_name = match elem { 33 | Some((idx, _)) => &name[idx + 1..name.len()], 34 | None => { 35 | eprintln!("{} Not enough length: {}", line!(), name); 36 | name 37 | } 38 | }; 39 | match new_name.chars().take(1).next() { 40 | None => { 41 | eprintln!("{} Empty string {}", line!(), name); 42 | name 43 | } 44 | // If after first pass the name starts with a digit (illegal name) do another pass 45 | Some(c) if c.is_digit(10) => match self { 46 | StripMode::StripFront(v) => StripMode::StripFront(v + 1), 47 | StripMode::KeepTail(v) => StripMode::KeepTail(v + 1), 48 | } 49 | .strip_long_name(name), 50 | Some(_) => new_name, 51 | } 52 | } 53 | } 54 | 55 | static ENUMS: Lazy> = Lazy::new(|| { 56 | // -N translates to StripMode::StripFront(N) 57 | // N translates to StripMode::KeepFront(N) 58 | let mut map = HashMap::new(); 59 | map.insert("HAPI_License", ("auto", -2)); 60 | map.insert("HAPI_Result", ("HapiResult", 2)); 61 | map.insert("HAPI_StatusType", ("auto", -2)); 62 | map.insert("HAPI_State", ("auto", 2)); 63 | map.insert("HAPI_PDG_WorkItemState", ("PdgWorkItemState", -1)); 64 | map.insert("HAPI_PDG_EventType", ("PdgEventType", -3)); 65 | map.insert("HAPI_PDG_State", ("PdgState", -1)); 66 | map.insert("HAPI_CacheProperty", ("auto", -2)); 67 | map.insert("HAPI_EnvIntType", ("auto", -2)); 68 | map.insert("HAPI_PrmScriptType", ("auto", -2)); 69 | map.insert("HAPI_Permissions", ("auto", -2)); 70 | map.insert("HAPI_ParmType", ("auto", 2)); 71 | 72 | map.insert("HAPI_PartType", ("auto", -1)); 73 | map.insert("HAPI_StatusVerbosity", ("auto", -1)); 74 | map.insert("HAPI_SessionType", ("auto", -1)); 75 | map.insert("HAPI_PackedPrimInstancingMode", ("auto", -1)); 76 | map.insert("HAPI_RampType", ("auto", -1)); 77 | map.insert("HAPI_ErrorCode", ("auto", -1)); 78 | map.insert("HAPI_NodeFlags", ("auto", -1)); 79 | map.insert("HAPI_NodeType", ("auto", -1)); 80 | map.insert("HAPI_HeightFieldSampling", ("auto", -1)); 81 | map.insert("HAPI_SessionEnvIntType", ("auto", -1)); 82 | map.insert("HAPI_ImagePacking", ("auto", -1)); 83 | map.insert("HAPI_ImageDataFormat", ("auto", -1)); 84 | map.insert("HAPI_XYZOrder", ("auto", -1)); 85 | map.insert("HAPI_RSTOrder", ("auto", -1)); 86 | map.insert("HAPI_TransformComponent", ("auto", -1)); 87 | map.insert("HAPI_CurveOrders", ("auto", -1)); 88 | map.insert("HAPI_InputType", ("auto", -1)); 89 | map.insert("HAPI_GeoType", ("auto", -1)); 90 | map.insert("HAPI_AttributeTypeInfo", ("auto", -1)); 91 | map.insert("HAPI_StorageType", ("auto", -1)); 92 | map.insert("HAPI_VolumeVisualType", ("auto", -1)); 93 | map.insert("HAPI_VolumeType", ("auto", -1)); 94 | map.insert("HAPI_CurveType", ("auto", -1)); 95 | map.insert("HAPI_AttributeOwner", ("auto", -1)); 96 | map.insert("HAPI_GroupType", ("auto", -1)); 97 | map.insert("HAPI_PresetType", ("auto", -1)); 98 | map.insert("HAPI_ChoiceListType", ("auto", -1)); 99 | map.insert("HAPI_InputCurveMethod", ("auto", -1)); 100 | map.insert("HAPI_InputCurveParameterization", ("auto", -1)); 101 | map 102 | }); 103 | 104 | #[derive(Debug)] 105 | struct Rustifier { 106 | visited: RefCell>>, 107 | } 108 | 109 | impl ParseCallbacks for Rustifier { 110 | fn enum_variant_name( 111 | &self, 112 | _enum_name: Option<&str>, 113 | _variant_name: &str, 114 | _variant_value: EnumVariantValue, 115 | ) -> Option { 116 | if _enum_name.is_none() { 117 | return None; 118 | }; 119 | let name = _enum_name 120 | .unwrap() 121 | .strip_prefix("enum ") 122 | .expect("Not enum?"); 123 | self.visited 124 | .borrow_mut() 125 | .entry(name.to_string()) 126 | .and_modify(|variants| variants.push(_variant_name.to_string())) 127 | .or_default(); 128 | let (_, _mode) = ENUMS.get(name).expect(&format!("Missing enum: {}", name)); 129 | let mode = StripMode::new(*_mode); 130 | let mut striped = mode.strip_long_name(_variant_name); 131 | // Two stripped variant names can collide with each other. We take a dumb approach by 132 | // attempting to strip one more time with increased step 133 | if let Some(vars) = self.visited.borrow_mut().get_mut(name) { 134 | let _stripped = striped.to_string(); 135 | if vars.contains(&_stripped) { 136 | let mode = StripMode::new(*_mode - 1); 137 | striped = mode.strip_long_name(_variant_name); 138 | } else { 139 | vars.push(_stripped); 140 | } 141 | } 142 | Some(heck::AsUpperCamelCase(striped).to_string()) 143 | } 144 | 145 | fn item_name(&self, _item_name: &str) -> Option { 146 | if let Some((rename, _)) = ENUMS.get(_item_name) { 147 | let new_name = match *rename { 148 | "auto" => _item_name 149 | .strip_prefix("HAPI_") 150 | .expect(&format!("{} - not a HAPI enum?", rename)), 151 | n => n, 152 | }; 153 | return Some(new_name.to_string()); 154 | } 155 | None 156 | } 157 | } 158 | 159 | fn main() { 160 | let hfs = PathBuf::from(&var("HFS").expect("HFS variable is not set")); 161 | let include_dir = hfs.join("toolkit/include/HAPI"); 162 | println!("cargo:rerun-if-changed=build.rs"); 163 | if cfg!(target_os = "macos") { 164 | let lib_dir = hfs.parent().unwrap().join("Libraries"); 165 | println!( 166 | "cargo:rustc-link-search=native={}", 167 | lib_dir.to_string_lossy() 168 | ); 169 | println!("cargo:rustc-link-lib=dylib=HAPIL"); 170 | } else if cfg!(target_os = "windows") { 171 | let lib_dir = hfs.join("custom/houdini/dsolib"); 172 | println!( 173 | "cargo:rustc-link-search=native={}", 174 | lib_dir.to_string_lossy() 175 | ); 176 | println!("cargo:rustc-link-lib=dylib=libHAPIL"); 177 | } else { 178 | println!("cargo:rustc-link-lib=dylib=HAPIL"); 179 | println!( 180 | "cargo:rustc-link-search=native={}/dsolib", 181 | hfs.to_string_lossy() 182 | ); 183 | } 184 | let out_path = PathBuf::from(var("OUT_DIR").unwrap()).join("bindings.rs"); 185 | 186 | let builder = bindgen::Builder::default() 187 | .header("wrapper.h") 188 | .clang_arg(format!("-I{}", include_dir.to_string_lossy())) 189 | .detect_include_paths(true) 190 | .default_enum_style("rust_non_exhaustive".parse().unwrap()) 191 | .bitfield_enum("NodeType") 192 | .bitfield_enum("NodeFlags") 193 | .bitfield_enum("ErrorCode") 194 | .prepend_enum_name(false) 195 | .generate_comments(false) 196 | .derive_copy(true) 197 | .derive_debug(true) 198 | .derive_hash(false) 199 | .derive_eq(false) 200 | .derive_partialeq(false) 201 | .disable_name_namespacing() 202 | .rustfmt_bindings(true) 203 | .layout_tests(false) 204 | .raw_line(format!( 205 | "// Houdini version {}", 206 | hfs.file_name().unwrap().to_string_lossy() 207 | )) 208 | .raw_line(format!( 209 | "// hapi-sys version {}", 210 | var("CARGO_PKG_VERSION").unwrap() 211 | )); 212 | let builder = if cfg!(feature = "rustify") { 213 | let callbacks = Box::new(Rustifier { 214 | visited: Default::default(), 215 | }); 216 | builder.parse_callbacks(callbacks) 217 | } else { 218 | builder 219 | }; 220 | builder 221 | .generate() 222 | .expect("bindgen failed") 223 | .write_to_file(out_path.clone()) 224 | .expect("Could not write bindings to file"); 225 | let link = out_path 226 | .parent() 227 | .unwrap() 228 | .parent() 229 | .unwrap() 230 | .parent() 231 | .unwrap() 232 | .join("bindings.rs"); 233 | if let Err(e) = std::fs::hard_link(&out_path, &link) { 234 | if e.kind() != std::io::ErrorKind::AlreadyExists { 235 | panic!("Could not create link: {}", &link.to_string_lossy()); 236 | } 237 | } 238 | println!("cargo:warning=Output: {}", &link.to_string_lossy()); 239 | } 240 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | 5 | //! # Info 6 | //! Raw bindings to Houdini Engine C API 7 | //! A much more nicer, Rusty crate hapi-rs is using these bindings and currently in WIP. 8 | //! 9 | //! There's a feature flag available called `rustify` which makes enums and their variants less ugly, 10 | //! for example `HAPI_Result::HAPI_RESULT_SUCCESS` becomes `HapiResult::Success` 11 | //! 12 | //! # Building 13 | //! HFS variable must be set, which is used to find Houdini header files and libraries 14 | //! 15 | //! # Running Tests 16 | //! `env LD_LIBRARY_PATH=$HDSO cargo test` // on Linux 17 | //! 18 | //! `env DYLD_FALLBACK_LIBRARY_PATH=$HDSO cargo test` // on Mac 19 | //! 20 | //! On Windows, make sure $HFS/bin is in $PATH 21 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 22 | 23 | #[cfg(test)] 24 | /// A simple test to initialize a session. 25 | /// Require libHAPI to run: 26 | /// Linux: 27 | /// MacOS: 28 | mod test { 29 | use std::mem::MaybeUninit; 30 | 31 | use super::*; 32 | 33 | #[test] 34 | fn basic_test() { 35 | unsafe { 36 | let mut ses = MaybeUninit::uninit(); 37 | let res = HAPI_CreateInProcessSession(ses.as_mut_ptr()); 38 | assert_eq!(res, HAPI_Result::HAPI_RESULT_SUCCESS); 39 | let ses = ses.assume_init(); 40 | assert_eq!(ses.type_, HAPI_SessionType::HAPI_SESSION_INPROCESS); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef HAPI_RS_WRAPPER_H 2 | #define HAPI_RS_WRAPPER_H 3 | 4 | #include "HAPI.h" 5 | #include "HAPI_Common.h" 6 | #include "HAPI_Version.h" 7 | #include "HAPI_Helpers.h" 8 | #include "HAPI_API.h" 9 | 10 | #endif //HAPI_RS_WRAPPER_H 11 | --------------------------------------------------------------------------------