diff --git a/rust-ffi-demo/.gitignore b/rust-ffi-demo/.gitignore new file mode 100644 index 0000000..3ced3e3 --- /dev/null +++ b/rust-ffi-demo/.gitignore @@ -0,0 +1,2 @@ +demo +demo.h \ No newline at end of file diff --git a/rust-ffi-demo/Makefile b/rust-ffi-demo/Makefile new file mode 100644 index 0000000..fcbb712 --- /dev/null +++ b/rust-ffi-demo/Makefile @@ -0,0 +1,9 @@ +LDFLAGS := -L../src/rust-base/target/debug -lstorybook -Wl,--gc-sections -lpthread -ldl +.PHONY: all clean +all: demo +demo: demo.h demo.c + $(CC) -g $^ $(LDFLAGS) -o $@ +demo.h: + cbindgen -c ../src/rust-base/cbindgen.toml --crate storybook --lang c -o demo.h ../src/rust-base +clean: + rm -f demo demo.h diff --git a/rust-ffi-demo/demo.c b/rust-ffi-demo/demo.c new file mode 100644 index 0000000..9e7de27 --- /dev/null +++ b/rust-ffi-demo/demo.c @@ -0,0 +1,47 @@ +#include +#include + +#include "demo.h" + +const char* story = "START\n" + "One day, Dane took a really big shit!\n" + "GOTO DIALOG\n" + "DIALOG\n" + "\"Wow, that was a really big shit!\"\n" + "1) Die [DIE]\n" + "2) Don't die [DONT_DIE] (-1 Honor)\n" + "DONT_DIE\n" + "But he died anyways.\n" + "GOTO DIE\n" + "DIE\n" + "Then he died. Fuck you, Dane!\n" + "THE END\n"; + +int main() { + Book *book = storybook_make_book(story); + + while(!storybook_is_ended(book)) { + char* s = storybook_get_body(book); + printf("%s\n", s); + storybook_free_string(s); + + enum footer footer = storybook_get_footer(book); + switch (footer) { + case (FOOTER_ENDING): + printf("The End\n"); + storybook_advance_nooption(book); + break; + case (FOOTER_GOTO): + storybook_advance_nooption(book); + break; + case (FOOTER_CHOICES): + uint32_t option; + printf("Make a choice: "); + scanf("%d", &option); + storybook_advance_option(book, option); + } + } + + storybook_free_book(book); + return 0; +} diff --git a/src/rust-base/Cargo.toml b/src/rust-base/Cargo.toml index 8e4f7f2..53cf25c 100644 --- a/src/rust-base/Cargo.toml +++ b/src/rust-base/Cargo.toml @@ -3,8 +3,12 @@ name = "storybook" version = "0.1.0" edition = "2021" +[lib] +crate-type = ["staticlib"] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + [dependencies] -peg = "0.8.0" +peg = "0.8.0" \ No newline at end of file diff --git a/src/rust-base/cbindgen.toml b/src/rust-base/cbindgen.toml new file mode 100644 index 0000000..a672638 --- /dev/null +++ b/src/rust-base/cbindgen.toml @@ -0,0 +1,8 @@ +language = "C" + +[export.rename] + +"FooterType" = "footer" + +[enum] +rename_variants = "QualifiedScreamingSnakeCase" \ No newline at end of file diff --git a/src/rust-base/my_header.h b/src/rust-base/my_header.h deleted file mode 100644 index 27e7e2a..0000000 --- a/src/rust-base/my_header.h +++ /dev/null @@ -1,19 +0,0 @@ -#include -#include -#include -#include -#include - -struct Page; - -template -struct Result; - -template -struct Vec; - -extern "C" { - -Result, ParseError> parse_story(const str *string); - -} // extern "C" diff --git a/src/rust-base/src/lib.rs b/src/rust-base/src/lib.rs index 1a57fcc..9ea823b 100644 --- a/src/rust-base/src/lib.rs +++ b/src/rust-base/src/lib.rs @@ -1,3 +1,7 @@ +// Parsing + +use std::collections::HashMap; + #[derive(Debug, PartialEq)] pub struct Page { id: String, @@ -9,7 +13,7 @@ pub struct Page { pub enum Footer { Ending, Goto(String), - Choices(Vec) + Choices(HashMap), } #[derive(Debug, PartialEq)] @@ -56,7 +60,7 @@ peg::parser! { rule footer() -> Footer = "THE END" __ { Footer::Ending } / "GOTO" _ i:id() __ { Footer::Goto(i) } / - c:choice()+ { Footer::Choices(c) } + c:choice()+ { Footer::Choices(c.into_iter().map(|x| (x.option, x)).collect()) } rule choice() -> Choice = o:$(['0'..='9']+) ")" _ f:$((!redirect() [_])+) r:redirect() __ { let (redirect, stat_change, stat_check) = r; @@ -94,10 +98,154 @@ peg::parser! { } } -pub fn parse_story(string: &str) -> Result, - peg::error::ParseError< - peg::str::LineCol>> { - storybook_parser::story(string) +pub struct Book { + pages: HashMap, + current_page: Option +} + +impl Book { + pub fn find (&mut self, id: &str) { + if self.pages.contains_key(id) { + self.current_page = Some(String::from(id)); + } else { + self.current_page = None + } + } + pub fn is_ended(&self) -> bool { + self.current_page == None + } + pub fn get_current(&self) -> &Page { + match &self.current_page { + None => panic!("Asked for current page, but current page is None!"), + Some(s) => self.pages.get(s).unwrap(), + } + } + pub fn advance_nooption(&mut self) { + let footer = &self.get_current().footer; + match footer { + Footer::Ending => {self.current_page = None}, + Footer::Goto(s) => { + let s = s.clone(); + self.find(&s); + }, + Footer::Choices(_) => panic!("No option for advance on choices page!"), + } + } + pub fn advance_option(&mut self, option: u32) { + let footer = &self.get_current().footer; + match footer { + Footer::Ending => panic!("Option provided for advance on ending page!"), + Footer::Goto(_) => panic!("Option provided for advance on goto page!"), + Footer::Choices(choices) => { + // TODO ban choices with failing stat checks + // TODO update stats based on stat changes + match choices.get(&option) { + Some(choice) => { + let s = choice.redirect.clone(); + self.find(&s); + } + None => panic!("Option not a valid choice for page!"), + } + } + } + } +} + +impl From> for Book { + fn from(v: Vec) -> Book { + let it = v.into_iter().map(|p| (p.id.clone(), p)); + let pages = HashMap::from_iter(it); + let mut book = Book { + pages, + current_page: None, + }; + + book.find("START"); + + book + } +} + +// C API + +use std::ffi::CStr; +use std::ffi::CString; +use std::os::raw::c_char; + +#[no_mangle] +pub extern fn storybook_make_book(string: *const c_char) -> *mut Book { + let string = unsafe { + CStr::from_ptr(string) + }; + let string = string.to_str().expect("Error, input string not valid UTF-8"); + let pages = storybook_parser::story(string).expect("Error, input string could not be parsed"); + let book = Book::from(pages); + let book = Box::from(book); + Box::into_raw(book) +} + +#[no_mangle] +pub extern fn storybook_free_book(book: *mut Book) { + unsafe { + Box::from_raw(book); + } +} + +#[no_mangle] +pub extern fn storybook_free_string(s: *mut c_char) { + unsafe { + drop(CString::from_raw(s)); + } +} + +fn get_book_from_ref<'a> (book: *mut Book) -> &'a mut Book { + let book = unsafe { + book.as_mut().expect("Invalid Book pointer") + }; + book +} + +#[no_mangle] +pub extern fn storybook_get_body(book: *mut Book) -> *mut c_char { + let book = get_book_from_ref(book); + let body = book.get_current().body.clone(); + let body = CString::new(body).expect("Body cannot be converted to CString"); + body.into_raw() +} + +#[repr(C)] +pub enum FooterType { + Ending, + Goto, + Choices, +} + +#[no_mangle] +pub extern fn storybook_get_footer(book: *mut Book) -> FooterType { + let book = get_book_from_ref(book); + match book.get_current().footer { + Footer::Ending => FooterType::Ending, + Footer::Goto(_) => FooterType::Goto, + Footer::Choices(_) => FooterType::Choices, + } +} + +#[no_mangle] +pub extern fn storybook_is_ended(book: *mut Book) -> bool { + let book = get_book_from_ref(book); + book.is_ended() +} + +#[no_mangle] +pub extern fn storybook_advance_nooption(book: *mut Book) { + let book = get_book_from_ref(book); + book.advance_nooption(); + +} +#[no_mangle] +pub extern fn storybook_advance_option(book: *mut Book, option: u32) { + let book = get_book_from_ref(book); + book.advance_option(option); } #[cfg(test)] @@ -128,7 +276,7 @@ THE END #[test] fn storybook_parser_test() { assert_eq!( - parse_story(STORY), + storybook_parser::story(STORY), Ok(vec![ Page { id: String::from("START"), @@ -138,15 +286,15 @@ THE END Page { id: String::from("DIALOG"), body: String::from("Dane: 'Wow, that was a really big shit!'"), - footer: Footer::Choices(vec![ - Choice { + footer: Footer::Choices(HashMap::from([ + (1, Choice { option: 1, flavor: String::from("Die"), redirect: String::from("DIE"), stat_check: None, stat_change: None, - }, - Choice { + }), + (2, Choice { option: 2, flavor: String::from("Don't die"), redirect: String::from("DONTDIE"), @@ -159,8 +307,8 @@ THE END value: 100, rel: '+', }) - } - ]) + }), + ])) }, Page { id: String::from("DIE"), @@ -170,4 +318,11 @@ THE END ]) ); } + + #[test] + fn test_book_from_pages() { + let pages = storybook_parser::story(STORY).unwrap(); + let book = Book::from(pages); + assert_eq!(book.current_page, Some(String::from("START"))); + } }