feat(treewide): Provide a c api

This commit is contained in:
Benedikt Peetz 2023-12-25 22:23:52 +01:00
parent 0744c8468d
commit 7d1a41aca9
Signed by: bpeetz
GPG Key ID: A5E94010C3A642AD
44 changed files with 1456 additions and 247 deletions

4
.gitignore vendored
View File

@ -2,8 +2,10 @@
/target /target
/result /result
# direnv # dev env
.direnv .direnv
.ccls-cache
# trixy is a library # trixy is a library
Cargo.lock Cargo.lock

View File

@ -102,7 +102,7 @@ comments:
commenter: commenter:
type: line type: line
comment_char: "//" comment_char: "//"
trailing_lines: 2 trailing_lines: 1
- extensions: - extensions:
- rs - rs
@ -124,7 +124,7 @@ comments:
start_block_char: "/*\n" start_block_char: "/*\n"
end_block_char: "*/\n" end_block_char: "*/\n"
per_line_char: "*" per_line_char: "*"
trailing_lines: 2 trailing_lines: 1
# In this case extension is singular and a single string extension is provided. # In this case extension is singular and a single string extension is provided.
- extension: html - extension: html
@ -139,7 +139,7 @@ comments:
commenter: commenter:
type: line type: line
comment_char: ";;;" comment_char: ";;;"
trailing_lines: 2 trailing_lines: 1
- extensions: - extensions:
- ebnf - ebnf
@ -148,7 +148,7 @@ comments:
start_block_char: "#(*\n" start_block_char: "#(*\n"
end_block_char: "#*)\n" end_block_char: "#*)\n"
per_line_char: "#" per_line_char: "#"
trailing_lines: 2 trailing_lines: 1
# The extension string "any" is special and so will match any file # The extension string "any" is special and so will match any file
# extensions. Commenter configurations are always checked in the # extensions. Commenter configurations are always checked in the
@ -162,4 +162,4 @@ comments:
commenter: commenter:
type: line type: line
comment_char: '#' comment_char: '#'
trailing_lines: 2 trailing_lines: 1

View File

@ -22,13 +22,12 @@ name = "trixy"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[lib]
proc-macro = true
[dependencies] [dependencies]
convert_case = "0.6.0" convert_case = "0.6.0"
log = "0.4.20"
proc-macro2 = "1.0.70" proc-macro2 = "1.0.70"
quote = "1.0.33" quote = "1.0.33"
syn = { version = "2.0.41", features = ["extra-traits", "full", "parsing"] } syn = { version = "2.0.41", features = ["extra-traits", "full", "parsing"] }
trixy-lang_parser = { path = "./trixy-lang_parser" } thiserror = "1.0.51"
trixy-macros = { path = "./trixy-macros" } # trixy-lang_parser = { path = "./trixy-lang_parser" }
# trixy-macros = { path = "./trixy-macros" }

View File

@ -17,6 +17,7 @@
# If not, see <https://www.gnu.org/licenses/>. # If not, see <https://www.gnu.org/licenses/>.
{ {
description = "A rust crate used to generate multi-language apis for your description = "A rust crate used to generate multi-language apis for your
application"; application";
@ -122,6 +123,10 @@
cargo-edit cargo-edit
cargo-expand cargo-expand
# Used to format the c header files
libclang
gdb
ebnf2pdf.outputs.packages."${system}".default ebnf2pdf.outputs.packages."${system}".default
]; ];
inherit nativeBuildInputs buildInputs; inherit nativeBuildInputs buildInputs;

View File

@ -18,23 +18,15 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::ffi::c_char;
nasp trinitrix { use thiserror::Error;
struct Callback {
func: String,
timeout: String,
};
enum CallbackPriority { #[derive(Error, Debug)]
High, pub enum TypeConversionError {
Medium, #[error("Can't convert this ('{got:#?}') string into a valid rust string; Remember that these must be valid utf-8")]
Low, String { got: *const c_char },
};
fn execute_callback(callback: Callback, priority: CallbackPriority); #[error("You passed a null pointer to the conversion function!")]
NullPointer,
} }
// That's a flat out lie, but it results in a rather nice syntax highlight compared to nothing:
// vim: syntax=rust

View File

@ -17,3 +17,28 @@
* and the Lesser GNU General Public License along with this program. * and the Lesser GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
//! Trixy contains the types used by the [trixy-macros] crate to provide ffi safe types
use std::{ffi::c_char, ops::Deref};
use proc_macro2::TokenStream;
use quote::quote;
pub mod error;
pub mod traits;
// NOTE(@soispha): All types specified here *MUST* be include in the BASE_TYPES constant, otherwise
// they are not usable from Trixy code <2023-12-25>
#[repr(C)]
pub struct String(*const c_char);
/// These are the "primitive" types used in Trixy, you can use any of them to create new structures
pub const BASE_TYPES: [&'static str; 2] = ["String", "u8"];
pub fn to_c_name<T: Deref<Target = str>>(rust_type: T) -> TokenStream {
match &*rust_type {
"String" => quote!(const char*),
other => panic! {"'{}' is not a vaild type name!", other},
}
}

152
src/traits/errno.rs Normal file
View File

@ -0,0 +1,152 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* This file is part of the Trixy crate for Trinitrix.
*
* Trixy is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* and the Lesser GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
use core::slice;
use std::{
cell::RefCell,
error::Error,
ffi::{c_char, c_int},
ptr,
};
use log::{error, warn};
use crate::error::TypeConversionError;
#[macro_export]
macro_rules! convert {
($input:expr) => {
match $input.try_into() {
Ok(ok) => ok,
Err(err) => {
trixy::traits::errno::set(err);
return 0;
}
}
};
}
// This code is heavily inspired by: https://michael-f-bryan.github.io/rust-ffi-guide/errors/return_types.html
thread_local! {
static LAST_ERROR: RefCell<Option<Box<TypeConversionError>>> = RefCell::new(None);
}
/// Update the most recent error, clearing whatever may have been there before.
pub fn set(error: TypeConversionError) {
error!("Setting LAST_ERROR: {}", error);
{
// Print a pseudo-backtrace for this error, following back each error's
// cause until we reach the root error.
let mut cause = error.source();
while let Some(parent_err) = cause {
warn!("Caused by: {}", parent_err);
cause = parent_err.source();
}
}
LAST_ERROR.with(|prev| {
*prev.borrow_mut() = Some(Box::new(error));
});
}
/// Retrieve the most recent error, clearing it in the process.
pub fn take_last_error() -> Option<Box<TypeConversionError>> {
LAST_ERROR.with(|prev| prev.borrow_mut().take())
}
pub const ERROR_FUNCTIONS: &'static str = r#"
/// Calculate the number of bytes in the last error's error message **not**
/// including any trailing `null` characters.
extern int last_error_length();
/// Write the most recent error message into a caller-provided buffer as a UTF-8
/// string, returning the number of bytes written.
///
/// # Note
///
/// This writes a **UTF-8** string into the buffer. Windows users may need to
/// convert it to a UTF-16 "unicode" afterwards.
///
/// If there are no recent errors then this returns `0` (because we wrote 0
/// bytes). `-1` is returned if there are any errors, for example when passed a
/// null pointer or a buffer of insufficient size.
extern int last_error_message(char* buffer, int length);
"#;
/// Calculate the number of bytes in the last error's error message **not**
/// including any trailing `null` characters.
#[no_mangle]
pub extern "C" fn last_error_length() -> c_int {
LAST_ERROR.with(|prev| match *prev.borrow() {
Some(ref err) => err.to_string().len() as c_int + 1,
None => 0,
})
}
/// Write the most recent error message into a caller-provided buffer as a UTF-8
/// string, returning the number of bytes written.
///
/// # Note
///
/// This writes a **UTF-8** string into the buffer. Windows users may need to
/// convert it to a UTF-16 "unicode" afterwards.
///
/// If there are no recent errors then this returns `0` (because we wrote 0
/// bytes). `-1` is returned if there are any errors, for example when passed a
/// null pointer or a buffer of insufficient size.
#[no_mangle]
pub unsafe extern "C" fn last_error_message(buffer: *mut c_char, length: c_int) -> c_int {
if buffer.is_null() {
warn!("Null pointer passed into last_error_message() as the buffer");
return -1;
}
let last_error = match take_last_error() {
Some(err) => err,
None => return 0,
};
let error_message = last_error.to_string();
let buffer = slice::from_raw_parts_mut(buffer as *mut u8, length as usize);
if error_message.len() >= buffer.len() {
warn!("Buffer provided for writing the last error message is too small.");
warn!(
"Expected at least {} bytes but got {}",
error_message.len() + 1,
buffer.len()
);
return -1;
}
ptr::copy_nonoverlapping(
error_message.as_ptr(),
buffer.as_mut_ptr(),
error_message.len(),
);
// Add a trailing null so people using the string as a `char *` don't
// accidentally read into garbage.
buffer[error_message.len()] = 0;
error_message.len() as c_int
}

55
src/traits/mod.rs Normal file
View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* This file is part of the Trixy crate for Trinitrix.
*
* Trixy is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* and the Lesser GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
use std::ffi::CStr;
use crate::error::TypeConversionError;
pub mod errno;
impl TryFrom<crate::String> for &str {
type Error = crate::error::TypeConversionError;
fn try_from(value: crate::String) -> Result<Self, Self::Error> {
let ptr = value.0;
if ptr.is_null() {
Err(TypeConversionError::NullPointer)
} else {
// SAFETY: We checked for null, which is a huge upside other things that we simply hope
// for:
// - null terminated
// - actually be a valid pointer (`(void*) 2` is *valid* c but not a *valid* pointer)
// - be contained in a single allocated object
// - the memory must obviously not be mutated, while this &str exists
// - the null terminator must be within `isize::MAX` from the start position
let str = unsafe { CStr::from_ptr(ptr) };
str.to_str()
.map_err(|_err| crate::error::TypeConversionError::String { got: value.0 })
}
}
}
impl TryFrom<crate::String> for String {
type Error = crate::error::TypeConversionError;
fn try_from(value: crate::String) -> Result<Self, Self::Error> {
let str: &str = value.try_into()?;
Ok(str.to_owned())
}
}

View File

@ -25,7 +25,17 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
clap = { version = "4.4.11", features = ["derive"] } clap = { version = "4.4.11", features = ["derive"], optional = true }
convert_case = "0.6.0" convert_case = "0.6.0"
pretty_assertions = "1.4.0"
thiserror = "1.0.50" thiserror = "1.0.50"
trixy = { path = "../../trixy" }
[dev-dependencies]
pretty_assertions = "1.4.0"
[features]
build-binary = ["clap"]
[[bin]]
name = "trixy-parser"
required-features = ["build-binary"]

View File

@ -2,5 +2,9 @@
This crate contains a parser (and lexer) for the Trixy language. This crate contains a parser (and lexer) for the Trixy language.
The corresponding grammar is in the grammar file [here](./docs/grammar.ebnf) encoded in [Extended Backus-Naur Form](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form). The corresponding grammar is in the grammar file [here](./docs/grammar.ebnf) encoded in [Extended Backus-Naur Form](https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form).
## Testing
A binary (`trixy-parser`) exists, which provides easy access to the different library
parsing steps
## Docs ## Docs
Run `./generate_docs` to turn the grammar file into railroad diagrams. Run `./generate_docs` to turn the grammar file into railroad diagrams.

View File

@ -19,3 +19,9 @@
*/ */
// an empty comment:
//
//

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::{fs, process::exit}; use std::{fs, process::exit};
use trixy_lang_parser::{lexing::TokenStream, parse_trixy_lang}; use trixy_lang_parser::{lexing::TokenStream, parse_trixy_lang};

View File

@ -18,26 +18,17 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
//! This module contains the already type checked types. //! This module contains the already type checked types.
use std::fmt::Display; use std::fmt::{Display, Write};
use crate::lexing::TokenKind; use crate::lexing::TokenKind;
use super::unchecked; use super::unchecked;
/// These are the "primitive" types used in trixy, you can use any of them to create new structures
pub const BASE_TYPES: [ConstIdentifier; 8] = [
Identifier::from("Integer"),
Identifier::from("Float"),
Identifier::from("Decimal"),
Identifier::from("String"),
Identifier::from("Function"),
Identifier::from("Option"),
Identifier::from("Result"),
Identifier::from("Vec"),
];
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Namespace { pub struct Namespace {
pub name: Identifier, pub name: Identifier,
@ -95,6 +86,28 @@ pub struct Type {
pub identifier: Identifier, pub identifier: Identifier,
pub generic_args: Vec<Type>, pub generic_args: Vec<Type>,
} }
impl Display for Type {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let ident = &self.identifier.name;
f.write_str(ident)?;
if !self.generic_args.is_empty() {
f.write_char('<')?;
let mut first_run = true;
for arg in &self.generic_args {
if !first_run {
f.write_str(", ")?;
} else {
first_run = false;
}
write!(f, "{}", arg)?;
}
f.write_char('>')
} else {
f.write_str("")
}
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct NamedType { pub struct NamedType {

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
pub mod checked; pub mod checked;
pub mod unchecked; pub mod unchecked;

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
//! This module contains the not type checked types. //! This module contains the not type checked types.
//! These are generated on the first pass of the parser, to be later converted into the checked //! These are generated on the first pass of the parser, to be later converted into the checked
//! ones. //! ones.

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use core::fmt; use core::fmt;
use thiserror::Error; use thiserror::Error;

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::{error::Error, fmt::Display}; use std::{error::Error, fmt::Display};
use thiserror::Error; use thiserror::Error;

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::fmt::Display; use std::fmt::Display;
use self::{error::SpannedLexingError, tokenizer::Tokenizer}; use self::{error::SpannedLexingError, tokenizer::Tokenizer};

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::lexing::{Keyword, Token, TokenKind, TokenSpan}; use crate::lexing::{Keyword, Token, TokenKind, TokenSpan};
use super::TokenStream; use super::TokenStream;

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
// This code is heavily inspired by: https://michael-f-bryan.github.io/static-analyser-in-rust/book/lex.html // This code is heavily inspired by: https://michael-f-bryan.github.io/static-analyser-in-rust/book/lex.html
use crate::{ use crate::{
@ -147,6 +150,18 @@ impl<'a> Tokenizer<'a> {
} }
} }
/// checks if the next char in the input str is a newline
fn end_of_line(text: &str) -> bool {
let next = text.chars().next();
if let Some('\n') = next {
true
} else if let Some('\r') = next {
true
} else {
false
}
}
fn tokenize_comment(text: &str) -> Result<(TokenKind, usize), LexingError> { fn tokenize_comment(text: &str) -> Result<(TokenKind, usize), LexingError> {
// every token starts with two slashes // every token starts with two slashes
let slashes: &str = &text[..2]; let slashes: &str = &text[..2];
@ -155,6 +170,9 @@ fn tokenize_comment(text: &str) -> Result<(TokenKind, usize), LexingError> {
} else { } else {
let text: &str = &text[2..]; let text: &str = &text[2..];
if let Some('/') = text.chars().next() { if let Some('/') = text.chars().next() {
if end_of_line(&text) {
Ok((TokenKind::DocComment("".to_owned()), 1 + 3))
} else {
let text = &text[1..]; let text = &text[1..];
let (doc_comment, chars_read) = take_while(text, |ch| ch != '\n' && ch != '\r')?; let (doc_comment, chars_read) = take_while(text, |ch| ch != '\n' && ch != '\r')?;
@ -162,11 +180,15 @@ fn tokenize_comment(text: &str) -> Result<(TokenKind, usize), LexingError> {
let doc_comment = doc_comment.trim_start(); let doc_comment = doc_comment.trim_start();
let doc_comment = doc_comment.trim_end(); let doc_comment = doc_comment.trim_end();
return Ok(( Ok((
TokenKind::DocComment(doc_comment.to_owned()), TokenKind::DocComment(doc_comment.to_owned()),
chars_read + 3, chars_read + 3,
)); ))
} }
} else {
if end_of_line(&text) {
Ok((TokenKind::Comment("".to_owned()), 1 + 2))
} else {
let (comment, chars_read) = take_while(text, |ch| ch != '\n' && ch != '\r')?; let (comment, chars_read) = take_while(text, |ch| ch != '\n' && ch != '\r')?;
// trim whitespace // trim whitespace
@ -176,6 +198,8 @@ fn tokenize_comment(text: &str) -> Result<(TokenKind, usize), LexingError> {
Ok((TokenKind::Comment(comment.to_owned()), chars_read + 2)) Ok((TokenKind::Comment(comment.to_owned()), chars_read + 2))
} }
} }
}
}
fn tokenize_ident(text: &str) -> Result<(TokenKind, usize), LexingError> { fn tokenize_ident(text: &str) -> Result<(TokenKind, usize), LexingError> {
let (got, chars_read) = take_while(text, |ch| ch == '_' || ch.is_alphanumeric())?; let (got, chars_read) = take_while(text, |ch| ch == '_' || ch.is_alphanumeric())?;

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use error::TrixyError; use error::TrixyError;
use crate::lexing::TokenStream; use crate::lexing::TokenStream;

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use thiserror::Error; use thiserror::Error;
use std::{error::Error, fmt::Display}; use std::{error::Error, fmt::Display};

View File

@ -18,15 +18,19 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::mem; use std::mem;
use convert_case::{Case, Casing}; use convert_case::{Case, Casing};
use trixy::BASE_TYPES;
use crate::{ use crate::{
command_spec::{ command_spec::{
checked::{ checked::{
CommandSpec, DocIdentifier, DocNamedType, Enumeration, Function, Identifier, NamedType, CommandSpec, DocIdentifier, DocNamedType, Enumeration, Function, Identifier, NamedType,
Namespace, Structure, Type, BASE_TYPES, Namespace, Structure, Type,
}, },
unchecked::{ unchecked::{
CommandSpec as UncheckedCommandSpec, DocNamedType as UncheckedDocNamedType, CommandSpec as UncheckedCommandSpec, DocNamedType as UncheckedDocNamedType,
@ -258,7 +262,7 @@ impl Parser {
.iter() .iter()
.map(|r#enum| Into::<Identifier>::into(r#enum.identifier.kind.clone())) .map(|r#enum| Into::<Identifier>::into(r#enum.identifier.kind.clone()))
.any(|ident| ident == identifier) .any(|ident| ident == identifier)
&& !BASE_TYPES.iter().any(|ident| ident.name == identifier.name) && !BASE_TYPES.iter().any(|ident| ident == &identifier.name)
{ {
return Err(ParsingError::TypeNotDeclared { return Err(ParsingError::TypeNotDeclared {
r#type: identifier, r#type: identifier,

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::command_spec::checked::{ use crate::command_spec::checked::{
Attribute, CommandSpec, DocIdentifier, DocNamedType, Enumeration, Function, Identifier, Attribute, CommandSpec, DocIdentifier, DocNamedType, Enumeration, Function, Identifier,
NamedType, Namespace, Structure, Type, NamedType, Namespace, Structure, Type,

View File

@ -18,5 +18,8 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
pub mod checked; pub mod checked;
pub mod unchecked; pub mod unchecked;

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::{error::Error, fmt::Display}; use std::{error::Error, fmt::Display};
use thiserror::Error; use thiserror::Error;

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::mem; use std::mem;
use crate::{ use crate::{

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use crate::{ use crate::{

View File

@ -22,12 +22,11 @@ name = "trixy-macros"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[lib]
proc-macro = true
[dependencies] [dependencies]
convert_case = "0.6.0" convert_case = "0.6.0"
prettyplease = "0.2.15"
proc-macro2 = "1.0.70" proc-macro2 = "1.0.70"
quote = "1.0.33" quote = "1.0.33"
syn = { version = "2.0.41", features = ["extra-traits", "full", "parsing"] } syn = { version = "2.0.41", features = ["extra-traits", "full", "parsing"] }
trixy-lang_parser = { path = "../trixy-lang_parser" } trixy-lang_parser = { path = "../trixy-lang_parser" }
trixy = { path = "../../trixy" }

7
trixy-macros/example/main/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
# build
/target
/result
/dist
# This crate is a library
Cargo.lock

View File

@ -0,0 +1,35 @@
# Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
#
# This file is part of the Trixy crate for Trinitrix.
#
# Trixy is free software: you can redistribute it and/or modify
# it under the terms of the Lesser GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# and the Lesser GNU General Public License along with this program.
# If not, see <https://www.gnu.org/licenses/>.
[package]
name = "main-example"
version = "0.0.0"
publish = false
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
trixy = { path = "../../../../trixy" }
env_logger = { version = "0.10.1" }
log = "0.4.20"
[build-dependencies]
trixy-macros = {path = "../../../trixy-macros"}

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* This file is part of the Trixy crate for Trinitrix.
*
* Trixy is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* and the Lesser GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
use trixy_macros::config::TrixyConfig;
fn main() {
println!("cargo:rerun-if-changed=./src/api.tri");
TrixyConfig::new("callback")
.trixy_path("./src/api.tri")
.dist_dir_path("./dist")
.generate_debug(true)
.generate();
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* This file is part of the Trixy crate for Trinitrix.
*
* Trixy is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* and the Lesser GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
#include "../dist/interface.h"
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int two;
} a_t;
int main(void) {
fn_alone("hi");
if (!fn_alone(0x0)) {
int error_length = last_error_length();
char *error = malloc(error_length);
last_error_message(error, error_length);
printf("Encountered error: %s\n", error);
free(error);
}
one.fn_one();
// one.two.fn_two();
//
// two_t two = one.two;
// two.three.fn_three();
// one.two.three.fn_three();
// one_two_fn_two();
return 0;
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* This file is part of the Trixy crate for Trinitrix.
*
* Trixy is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* and the Lesser GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
// nasp trinitrix {
// struct Callback {
// func: String,
// timeout: String,
// };
//
// enum CallbackPriority {
// High,
// Medium,
// Low,
// };
//
// fn execute_callback(callback: Callback, priority: CallbackPriority);
// }
/// Some doc comment
fn fn_alone(input: String);
/// Some doc comment
nasp one {
/// Some doc comment
fn fn_one();
nasp two {
fn fn_two();
nasp three {
fn fn_three();
}
}
}
// That's a flat out lie, but it results in a rather nice syntax highlight compared to nothing:
// vim: syntax=rust

View File

@ -19,12 +19,12 @@
*/ */
fn print(message: CommandTransferValue);
nasp trinitrix { {}
fn hi honner(name: String) -> String; ; macro_rules! callback {
($cmd:expr) => {{
println!("{:#?}", $cmd);
}};
} }
include!(concat!(env!("OUT_DIR"), "/api.rs"));
// That's a flat out lie, but it results in a rather nice syntax highlight compared to nothing:
// vim: syntax=rust

View File

@ -18,12 +18,8 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use trixy_macros::trixy_generate;
fn main() { fn main() {
trixy_generate! { let input = include_str!(concat!(env!("OUT_DIR"), "/interface.h"));
path: "./examples/main/api.tri" println!("{}", input);
languages: rust, lua, c
generate_debug: false
}
} }

View File

@ -1 +0,0 @@
api_correct.tri

View File

@ -18,138 +18,119 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
//! This module is responsible for parsing the config passed to the macro call:
//! For example:
//! This module is responsible for the config passed to trixy.
//! It works using the popular builder syntax:
//! ```no_run //! ```no_run
//! trixy_generate! { //! use trixy_macros::config::{Language, TrixyConfig};
//! path: "./path/to/trixy/file.tri" //!# fn main() {
//! languages: rust, lua, c //! let config = TrixyConfig::new()
//! generate_debug: false //! .set_input_path("path/to/trixy/api.tri")
//! } //! .set_output_path("api.rs")
//! .set_languages(vec![Language::Rust, Language::C])
//! .set_generate_debug(false);
//!# }
//! ``` //! ```
use std::path::PathBuf; use std::path::{Path, PathBuf};
use proc_macro2::Ident;
use syn::{parse::Parse, punctuated::Punctuated, LitBool, LitStr, Result, Token};
mod kw {
syn::custom_keyword!(path);
syn::custom_keyword!(languages);
syn::custom_keyword!(generate_debug);
}
#[derive(Debug)] #[derive(Debug)]
pub enum Language { pub enum Language {
Rust, Rust,
Lua,
C, C,
Lua,
} }
#[derive(Debug)] #[derive(Default, Debug)]
struct Languages {
#[allow(dead_code)]
languages: kw::languages,
#[allow(dead_code)]
colon: Token![:],
raw: Punctuated<Language, Token![,]>,
}
#[derive(Debug)]
struct Path {
#[allow(dead_code)]
path: kw::path,
#[allow(dead_code)]
colon: Token![:],
raw: PathBuf,
}
#[derive(Debug)]
struct GenerateDebug {
#[allow(dead_code)]
generate_debug: kw::generate_debug,
#[allow(dead_code)]
colon: Token![:],
raw: bool,
}
#[derive(Debug)]
pub struct TrixyConfig { pub struct TrixyConfig {
/// The Path to the base command interface config file /// The Path to the base command interface config file
path: Path, pub trixy_path: PathBuf,
/// The languages the commands should be exposed in /// The name of the outputted host code (rust and c bindings)
languages: Languages, /// This file is written in $OUT_DIR
pub host_code_name: PathBuf,
/// The name of the c header
/// This file is written in $OUT_DIR
pub c_header_name: PathBuf,
/// The path from the root to the distribution directory.
/// Things like the c headers are copied in this dir
/// When this is [None] no dist dir will be generated
pub dist_dir_path: Option<PathBuf>,
/// Whether to check if the dist dir is empty before writing something to it
pub check_dist_dir: bool,
/// Should the macro generate Debug trait implementation for each enum /// Should the macro generate Debug trait implementation for each enum
/// These are very useful but completely obscure the `cargo expand` ouput /// These are very useful but completely obscure the `cargo expand` output
generate_debug: GenerateDebug, pub generate_debug: bool,
/// This function is executed whenever an API function is called.
/// The only argument is the command (encoded as the `Commands`) enum
/// which is represented by this function
/// Because of rust limitations this is supposed to be the name of a macro acting as said
/// function. This macro must be in scope of the generated code
pub callback_function: String,
} }
impl TrixyConfig { impl TrixyConfig {
pub fn get_path(&self) -> &std::path::Path { pub fn new<T: Into<String>>(callback_function: T) -> Self {
&self.path.raw Self {
} callback_function: callback_function.into(),
pub fn generate_debug(&self) -> bool { host_code_name: "api.rs".into(),
self.generate_debug.raw c_header_name: "interface.h".into(),
..Default::default()
} }
} }
impl Parse for TrixyConfig { pub fn trixy_path<T: Into<PathBuf>>(self, input_path: T) -> Self {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { Self {
Ok(Self { trixy_path: input_path.into(),
path: input.parse()?, ..self
languages: input.parse()?,
generate_debug: input.parse()?,
})
} }
} }
impl Parse for GenerateDebug { pub fn generate_debug(self, generate_debug: bool) -> Self {
fn parse(input: syn::parse::ParseStream) -> Result<Self> { Self {
let kw: kw::generate_debug = input.parse()?; generate_debug,
let colon: Token![:] = input.parse()?; ..self
let raw = input.parse::<LitBool>()?.value();
Ok(Self {
generate_debug: kw,
colon,
raw,
})
} }
} }
impl Parse for Path { pub fn dist_dir_path<T: Into<PathBuf>>(self, dist_dir_path: T) -> Self {
fn parse(input: syn::parse::ParseStream) -> Result<Self> { Self {
let path: kw::path = input.parse()?; dist_dir_path: Some(dist_dir_path.into()),
let colon: Token![:] = input.parse()?; ..self
let raw = PathBuf::from(input.parse::<LitStr>()?.value());
Ok(Self { path, colon, raw })
} }
} }
impl Parse for Languages { pub fn check_dist_dir(self, check_dist_dir: bool) -> Self {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { Self {
let languages: kw::languages = input.parse()?; check_dist_dir,
let colon: Token![:] = input.parse()?; ..self
let raw = Punctuated::<Language, Token![,]>::parse_separated_nonempty(input)?;
Ok(Self {
languages,
colon,
raw,
})
} }
} }
impl Parse for Language { pub fn host_code_name<T: Into<PathBuf>>(self, output_path: T) -> Self {
fn parse(input: syn::parse::ParseStream) -> Result<Self> { Self {
let ident: Ident = input.parse()?; host_code_name: output_path.into(),
match &ident.to_string()[..] { ..self
"rust" | "Rust" => Ok(Self::Rust), }
"lua" | "Lua" => Ok(Self::Lua), }
"c" | "C" => Ok(Self::C),
other => Err(input.error(format!( pub fn c_header_name<T: Into<PathBuf>>(self, output_path: T) -> Self {
"The language: `{}` is not a registered language!", Self {
other c_header_name: output_path.into(),
))), ..self
}
}
pub fn callback_function<T: Into<String>>(self, callback_function: T) -> Self {
Self {
callback_function: callback_function.into(),
..self
} }
} }
} }

View File

@ -0,0 +1,293 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* This file is part of the Trixy crate for Trinitrix.
*
* Trixy is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* and the Lesser GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
//! This module generates the c header
//! It works by firstly listing the functions and then by grouping them into structures, effectively
//! simulating namespaces in c.
use proc_macro2::{Ident, Punct, Spacing, TokenStream as TokenStream2};
use quote::{format_ident, quote};
use trixy_lang_parser::command_spec::{
Attribute, CommandSpec, Function, Identifier, NamedType, Namespace,
};
use crate::{
config::TrixyConfig,
generate::{c_api::mangle_c_function_ident, identifier_to_rust, type_to_rust},
};
const BEGIN_HEADER_GUARD: &'static str = r"#ifndef TRIXY_MAIN_HEADER
#define TRIXY_MAIN_HEADER";
const END_HEADER_GUARD: &'static str = r"#endif // ifndef TRIXY_MAIN_HEADER";
/// This function acts as the core transformative aspect, turning this trixy code into the
/// following c header:
///
/// *Trixy:*
/// ```text
/// fn fn_alone();
/// nasp one {
/// fn fn_one();
/// nasp two {
/// fn fn_two();
/// nasp three {
/// fn fn_three();
/// }
/// }
/// }
/// ```
/// *C header:*
/// ```text
/// #ifndef TRIXY_MAIN_HEADER
/// #define TRIXY_MAIN_HEADER
///
/// extern int fn_alone();
/// extern int one_fn_one();
/// extern int one_two_fn_two();
/// extern int one_two_three_fn_three();
///
/// typedef struct {
/// void (*fn_three)(void);
/// } three_t;
/// typedef struct {
/// void (*fn_two)(void);
/// three_t three;
/// } two_t;
/// typedef struct {
/// void (*fn_one)(void);
/// two_t two;
/// } one_t;
///
/// const three_t three = {
/// .fn_three = one_two_three_fn_three,
/// };
/// const two_t two = {
/// .fn_two = one_two_fn_two,
/// .three = three,
/// };
/// const one_t one = {
/// .fn_one = one_fn_one,
/// .two = two,
/// };
/// #endif // ifndef TRIXY_MAIN_HEADER
/// ```
pub fn generate(trixy: &CommandSpec, _config: &TrixyConfig) -> String {
let functions: String = trixy
.functions
.iter()
.map(|r#fn| function_to_header(r#fn, &[]))
.collect();
let namespaces: String = trixy
.namespaces
.iter()
.map(|nasp| namespace_to_header(nasp, &vec![]))
.collect();
let type_defs: String = trixy
.namespaces
.iter()
.rev()
.map(|nasp| namespace_to_full_typedef(nasp))
.collect::<Vec<String>>()
.join("\n");
let struct_initializer: TokenStream2 = trixy
.namespaces
.iter()
.map(|nasp| namespace_to_full_struct_init(nasp, &vec![]))
.collect();
let output = quote! {
#struct_initializer
}
.to_string();
format!(
"{}\n\n{}\n\n{}\n{}\n{}\n{}\n\n{}",
BEGIN_HEADER_GUARD,
trixy::traits::errno::ERROR_FUNCTIONS,
functions,
namespaces,
type_defs,
output,
END_HEADER_GUARD
)
}
fn function_to_header(function: &Function, namespaces: &[&Identifier]) -> String {
let doc_comments: String = function
.attributes
.iter()
.map(attribute_to_doc_comment)
.collect::<String>();
let ident = mangle_c_function_ident(function, namespaces);
let inputs: Vec<TokenStream2> = function.inputs.iter().map(named_type_to_c).collect();
let output = quote! {
extern int #ident(#(#inputs),*);
};
format!("{}{}\n", doc_comments, output)
}
fn attribute_to_doc_comment(attribute: &Attribute) -> String {
let Attribute::doc(doc_comment) = attribute;
format!("/// {}\n", doc_comment)
}
fn named_type_to_c(named_type: &NamedType) -> TokenStream2 {
let ident = identifier_to_rust(&named_type.name);
let r#type = type_to_rust(&named_type.r#type);
let c_type = trixy::to_c_name(r#type.to_string());
quote! {
#c_type #ident
}
}
fn namespace_to_full_struct_init(nasp: &Namespace, namespaces: &Vec<&Identifier>) -> TokenStream2 {
let mut input_namespaces = namespaces.clone();
input_namespaces.push(&nasp.name);
let ident = identifier_to_rust(&nasp.name);
let type_ident = format_ident!("{}_t", ident.to_string());
let functions: TokenStream2 = nasp
.functions
.iter()
.map(|r#fn| function_to_struct_init(r#fn, &input_namespaces))
.collect();
let namespaces: TokenStream2 = nasp
.namespaces
.iter()
.map(namespace_to_struct_init)
.collect();
let next_namespace: TokenStream2 = nasp
.namespaces
.iter()
.map(|nasp| namespace_to_full_struct_init(nasp, &input_namespaces))
.collect();
quote! {
#next_namespace
const #type_ident #ident = {
#functions
#namespaces
};
}
}
fn function_to_struct_init(function: &Function, namespaces: &[&Identifier]) -> TokenStream2 {
let ident = identifier_to_rust(&function.identifier);
let full_ident = mangle_c_function_ident(function, namespaces);
quote! {
. #ident = #full_ident,
}
}
fn namespace_to_struct_init(namespace: &Namespace) -> TokenStream2 {
let ident = identifier_to_rust(&namespace.name);
quote! {
. #ident = #ident ,
}
}
fn namespace_to_full_typedef(nasp: &Namespace) -> String {
let ident = format_ident!("{}_t", nasp.name.name);
let doc_comments = nasp
.attributes
.iter()
.map(attribute_to_doc_comment)
.collect::<String>();
let functions: TokenStream2 = nasp
.functions
.iter()
.map(|r#fn| function_to_typedef(r#fn))
.collect();
let namespaces: TokenStream2 = nasp
.namespaces
.iter()
.map(|nasp| namespace_to_typedef(nasp))
.collect();
let next_namespace: String = nasp
.namespaces
.iter()
.map(|nasp| namespace_to_full_typedef(nasp))
.collect::<Vec<String>>()
.join("\n");
let namespace = quote! {
typedef struct {
#functions
#namespaces
} #ident;
};
format! {"{}\n{}{}\n", next_namespace, doc_comments, namespace}
}
fn function_to_typedef(function: &Function) -> TokenStream2 {
let ident = identifier_to_rust(&function.identifier);
let output = if let Some(output) = &function.output {
let output = output.to_string();
quote! { #output, }
} else {
TokenStream2::default()
};
let inputs: Vec<TokenStream2> = if function.inputs.is_empty() {
vec![quote! { void }]
} else {
todo!()
};
quote! {
int (* #ident ) (#output #(#inputs),*);
}
}
fn namespace_to_typedef(namespace: &Namespace) -> TokenStream2 {
let ident = identifier_to_rust(&namespace.name);
let type_ident = format_ident!("{}_t", ident.to_string());
quote! {
#type_ident #ident ;
}
}
fn namespace_to_header(nasp: &Namespace, namespaces: &Vec<&Identifier>) -> String {
let mut nasps = namespaces.clone();
nasps.push(&nasp.name);
let functions: String = nasp
.functions
.iter()
.map(|r#fn| function_to_header(r#fn, &nasps))
.collect::<Vec<String>>()
.join("\n");
let namespaces: String = nasp
.namespaces
.iter()
.map(|nasp| namespace_to_header(nasp, &nasps))
.collect();
format! {"{}\n{}", functions, namespaces}
}

View File

@ -0,0 +1,291 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* This file is part of the Trixy crate for Trinitrix.
*
* Trixy is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* and the Lesser GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use trixy_lang_parser::command_spec::{CommandSpec, Function, Identifier, NamedType, Namespace};
use crate::{
config::TrixyConfig,
generate::{
c_api::mangle_c_function_ident, identifier_to_rust, named_type_to_rust, type_to_rust,
},
};
/// This function generates the main c API provided by Trixy.
/// This works for example like this:
/// Turning this:
/// ```text
/// nasp trinitrix {
/// struct Callback {
/// func: String,
/// timeout: String,
/// };
///
/// enum CallbackPriority {
/// High,
/// Medium,
/// Low,
/// };
///
/// fn execute_callback(callback: Callback, priority: CallbackPriority);
/// }
/// ```
/// to this:
/// ```no_run
/// pub extern "C" fn exectute_callback(callback: Callback, priority: CallbackPriority) {
/// /* Here we simply call your handler function, with the command of the function */
/// }
/// ```
pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> TokenStream2 {
let functions: TokenStream2 = trixy
.functions
.iter()
.map(|r#fn| function_to_c(r#fn, &config, &vec![]))
.collect();
let namespaced_functions: TokenStream2 = trixy
.namespaces
.iter()
.map(|nasp| namespace_to_c(&nasp, &config, &vec![]))
.collect();
quote! {
#functions
#namespaced_functions
}
}
fn namespace_to_c(
namespace: &Namespace,
config: &TrixyConfig,
namespaces: &Vec<&Identifier>,
) -> TokenStream2 {
let mut namespaces = namespaces.clone();
namespaces.push(&namespace.name);
let functions: TokenStream2 = namespace
.functions
.iter()
.map(|r#fn| function_to_c(&r#fn, &config, &namespaces))
.collect();
let additional_functions: TokenStream2 = namespace
.namespaces
.iter()
.map(|nasp| namespace_to_c(&nasp, &config, &namespaces))
.collect();
quote! {
#functions
#additional_functions
}
}
fn function_to_c(
function: &Function,
config: &TrixyConfig,
namespaces: &Vec<&Identifier>,
) -> TokenStream2 {
let ident = mangle_c_function_ident(function, namespaces);
let inputs: Vec<TokenStream2> = function
.inputs
.iter()
.map(named_type_to_rust_trixy)
.collect();
let callback_function = format_ident!("{}", config.callback_function);
let command_value: TokenStream2 = function_path_to_rust(&namespaces, &function);
if let Some(r#type) = &function.output {
let output = type_to_rust(&r#type);
quote! {
#[no_mangle]
pub unsafe extern "C" fn #ident(output: *mut #output, #(#inputs),*) -> core::ffi::c_int {
#callback_function ! (#command_value)
}
}
} else {
quote! {
#[no_mangle]
pub extern "C" fn #ident(#(#inputs),*) -> core::ffi::c_int {
#callback_function ! (#command_value);
return 1;
}
}
}
}
fn named_type_to_rust_trixy(named_type: &NamedType) -> TokenStream2 {
let ident = identifier_to_rust(&named_type.name);
let type_ident = type_to_rust(&named_type.r#type);
quote! {
#ident : trixy:: #type_ident
}
}
/// Turns a function in namespaces to the generated host enum path:
/// *trixy code:*
/// ```text
/// fn fn_alone();
/// nasp one {
/// fn fn_one();
/// nasp two {
/// fn fn_two(input: String);
/// }
/// }
/// ```
/// *rust enum path for fn_alone:*
/// ```no_run
/// Commands::fn_alone
/// ```
/// *rust enum path for fn_one:*
/// ```no_run
/// // `Commands` is just the name for the top-level namespace
/// Commands::One(one::One(
/// One::fn_one
/// ))
/// ```
/// *rust enum path for fn_two:*
/// ```no_run
/// Commands::One(one::One(
/// one::two::Two(one::two::Two(
/// Two::fn_two {input: String}
/// ))))
/// ```
fn function_path_to_rust(namespaces: &Vec<&Identifier>, function: &Function) -> TokenStream2 {
let function_ident = {
let ident = format_ident!("{}", function.identifier.name);
if function.inputs.is_empty() {
quote! {
#ident
}
} else {
let inputs: Vec<TokenStream2> = function
.inputs
.iter()
.map(named_type_to_rust_assignment)
.collect();
quote! {
#ident { #(#inputs),* }
}
}
};
if namespaces.is_empty() {
quote! {
Commands:: #function_ident
}
} else {
let nasp_pascal_ident = format_ident!(
"{}",
namespaces
.last()
.expect("We checked")
.name
.to_case(Case::Pascal)
);
let namespace_path = nasps_to_path(namespaces);
let function_call = if !function.inputs.is_empty() {
let inputs: Vec<TokenStream2> =
function.inputs.iter().map(named_type_to_rust).collect();
quote! {
#namespace_path :: #nasp_pascal_ident :: #function_ident { #(#inputs),* }
}
} else {
quote! {
#namespace_path :: #nasp_pascal_ident :: #function_ident
}
};
let output: TokenStream2 = namespaces
.iter()
.enumerate()
.rev()
.fold(function_call, |acc, (index, nasp)| {
nasp_path_one_part(nasp, &acc, &namespaces, index)
});
output
}
}
fn named_type_to_rust_assignment(named_type: &NamedType) -> TokenStream2 {
let ident = identifier_to_rust(&named_type.name);
quote! {
#ident : trixy::convert!(#ident)
}
}
/// This function add a namespace component to the [input] value like so:
/// (taking the example from the [function_path_to_rust] function)
/// ```text
/// one::two::Two::fn_two [= <input>]
/// ->
/// one::One::Two(<input>) [= <input>]
/// ->
/// Commands::One(<input>) [= <input>]
/// ```
fn nasp_path_one_part(
current_nasp: &Identifier,
input: &TokenStream2,
namespaces: &Vec<&Identifier>,
index: usize,
) -> TokenStream2 {
let namespaces_to_do = &namespaces[..index];
let ident_pascal = format_ident!("{}", current_nasp.name.to_case(Case::Pascal));
if index == 0 {
quote! {
Commands :: #ident_pascal ( #input )
}
} else {
let ident_pascal_next = format_ident!(
"{}",
namespaces_to_do
.last()
.expect("We checked the index")
.name
.to_case(Case::Pascal)
);
let namespace_path = nasps_to_path(namespaces_to_do);
quote! {
#namespace_path :: #ident_pascal_next :: #ident_pascal ( #input )
}
}
}
fn nasps_to_path(namespaces: &[&Identifier]) -> TokenStream2 {
namespaces
.iter()
.fold(TokenStream2::default(), |acc, nasp| {
let ident = format_ident!("{}", nasp.name);
if acc.is_empty() {
quote! {
#ident
}
} else {
quote! {
#acc :: #ident
}
}
})
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2023 The Trinitrix Project <soispha@vhack.eu, antifallobst@systemausfall.org>
*
* This file is part of the Trixy crate for Trinitrix.
*
* Trixy is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* and the Lesser GNU General Public License along with this program.
* If not, see <https://www.gnu.org/licenses/>.
*/
use proc_macro2::Ident;
use quote::format_ident;
use trixy_lang_parser::command_spec::{Function, Identifier};
pub mod header;
pub mod host;
pub fn mangle_c_function_ident(function: &Function, namespaces: &[&Identifier]) -> Ident {
let namespace_str = namespaces.iter().fold(String::default(), |acc, nasp| {
if acc.is_empty() {
nasp.name.clone()
} else {
format!("{}_{}", acc, nasp.name)
}
});
if namespace_str.is_empty() {
format_ident!("{}", &function.identifier.name)
} else {
format_ident!("{}_{}", namespace_str, &function.identifier.name)
}
}

View File

@ -18,6 +18,9 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
//! This module is responsible for generating the rust code used to interface with the api. //! This module is responsible for generating the rust code used to interface with the api.
//! That includes the structs and enums declared in the trixy file and the enum used to describe the //! That includes the structs and enums declared in the trixy file and the enum used to describe the
//! command being executed. //! command being executed.
@ -28,20 +31,23 @@ use convert_case::{Case, Casing};
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote}; use quote::{format_ident, quote};
use trixy_lang_parser::command_spec::{ use trixy_lang_parser::command_spec::{
Attribute, CommandSpec, DocIdentifier, DocNamedType, Enumeration, Function, Identifier, Attribute, CommandSpec, DocIdentifier, DocNamedType, Enumeration, Function, Namespace,
NamedType, Namespace, Structure, Type, Structure,
}; };
use crate::config::TrixyConfig; use crate::{
config::TrixyConfig,
generate::{identifier_to_rust, named_type_to_rust},
};
thread_local! {static DEBUG: OnceCell<TokenStream2> = OnceCell::new();} thread_local! {static DEBUG: OnceCell<TokenStream2> = OnceCell::new();}
/// This function turns, for example, the following trixy input into this rust code: /// This function turns, for example, the following trixy input into this rust code:
/// ```rust /// ```text
/// nasp trinitrix { /// nasp trinitrix {
/// struct Callback { /// struct Callback {
/// func: Function, /// func: String,
/// timeout: Integer, /// timeout: String,
/// }; /// };
/// ///
/// enum CallbackPriority { /// enum CallbackPriority {
@ -52,20 +58,22 @@ thread_local! {static DEBUG: OnceCell<TokenStream2> = OnceCell::new();}
/// ///
/// fn execute_callback(callback: Callback, priority: CallbackPriority); /// fn execute_callback(callback: Callback, priority: CallbackPriority);
/// } /// }
/// // wrong but helps with syntax highlight:
/// // vim: syntax=rust
/// ``` /// ```
/// ```rust /// ```no_run
/// #[derive(Debug)] /// #[derive(Debug)]
/// pub enum Commands { /// pub enum Commands {
/// Trinitrix(trinitrix::Trinitrix), /// Trinitrix(trinitrix::Trinitrix),
/// } /// }
/// pub mod trinitrix { /// pub mod trinitrix {
/// #[allow(non_camel_case)] /// #[allow(non_camel_case_types)]
/// #[derive(Debug)] /// #[derive(Debug)]
/// struct Callback { /// struct Callback {
/// func: Function, /// func: String,
/// timeout: Integer, /// timeout: String,
/// } /// }
/// #[allow(non_camel_case)] /// #[allow(non_camel_case_types)]
/// #[derive(Debug)] /// #[derive(Debug)]
/// enum CallbackPriority { /// enum CallbackPriority {
/// High, /// High,
@ -74,14 +82,14 @@ thread_local! {static DEBUG: OnceCell<TokenStream2> = OnceCell::new();}
/// } /// }
/// #[derive(Debug)] /// #[derive(Debug)]
/// pub enum Trinitrix { /// pub enum Trinitrix {
/// #[allow(non_camel_case)] /// #[allow(non_camel_case_types)]
/// execute_callback { callback: Callback, priority: CallbackPriority }, /// execute_callback { callback: Callback, priority: CallbackPriority },
/// } /// }
/// } /// }
/// ``` /// ```
pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> TokenStream2 { pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> TokenStream2 {
DEBUG.with(|d| { DEBUG.with(|d| {
d.set(if config.generate_debug() { d.set(if config.generate_debug {
quote! { quote! {
#[derive(Debug)] #[derive(Debug)]
} }
@ -190,14 +198,23 @@ fn function_to_rust(function: &Function) -> TokenStream2 {
.map(attribute_to_doc_comment) .map(attribute_to_doc_comment)
.collect(); .collect();
let ident = identifier_to_rust(&function.identifier); let ident = identifier_to_rust(&function.identifier);
let inputs: Vec<TokenStream2> = function.inputs.iter().map(named_type_to_rust).collect(); let inputs: Vec<TokenStream2> = function.inputs.iter().map(named_type_to_rust).collect();
if inputs.is_empty() {
quote! {
#doc_comments
#[allow(non_camel_case_types)]
#ident
}
} else {
quote! { quote! {
#doc_comments #doc_comments
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
#ident {#(#inputs),*} #ident {#(#inputs),*}
} }
} }
}
fn enumeration_to_rust(enumeration: &Enumeration) -> TokenStream2 { fn enumeration_to_rust(enumeration: &Enumeration) -> TokenStream2 {
let doc_comments: TokenStream2 = enumeration let doc_comments: TokenStream2 = enumeration
@ -216,7 +233,7 @@ fn enumeration_to_rust(enumeration: &Enumeration) -> TokenStream2 {
quote! { quote! {
#doc_comments #doc_comments
#[allow(non_camel_case)] #[allow(non_camel_case_types)]
#debug #debug
enum #ident { enum #ident {
#(#states),* #(#states),*
@ -241,7 +258,7 @@ fn structure_to_rust(structure: &Structure) -> TokenStream2 {
quote! { quote! {
#doc_comments #doc_comments
#[allow(non_camel_case)] #[allow(non_camel_case_types)]
#debug #debug
struct #ident { struct #ident {
#(#contents),* #(#contents),*
@ -263,13 +280,6 @@ fn doc_identifier_to_rust(doc_identifier: &DocIdentifier) -> TokenStream2 {
} }
} }
fn identifier_to_rust(identifier: &Identifier) -> TokenStream2 {
let ident = format_ident!("{}", &identifier.name);
quote! {
#ident
}
}
fn doc_named_type_to_rust(doc_named_type: &DocNamedType) -> TokenStream2 { fn doc_named_type_to_rust(doc_named_type: &DocNamedType) -> TokenStream2 {
let doc_comments: TokenStream2 = doc_named_type let doc_comments: TokenStream2 = doc_named_type
.attributes .attributes
@ -282,19 +292,3 @@ fn doc_named_type_to_rust(doc_named_type: &DocNamedType) -> TokenStream2 {
#named_type #named_type
} }
} }
fn named_type_to_rust(named_type: &NamedType) -> TokenStream2 {
let ident = identifier_to_rust(&named_type.name);
let r#type = type_to_rust(&named_type.r#type);
quote! {
#ident : #r#type
}
}
fn type_to_rust(r#type: &Type) -> TokenStream2 {
let ident = identifier_to_rust(&r#type.identifier);
let generics: Vec<TokenStream2> = r#type.generic_args.iter().map(type_to_rust).collect();
quote! {
#ident <#(#generics),*>
}
}

View File

@ -18,4 +18,55 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use trixy_lang_parser::command_spec::{CommandSpec, Identifier, NamedType, Type};
use crate::config::TrixyConfig;
pub mod c_api;
pub mod host; pub mod host;
pub fn generate(trixy: &CommandSpec, config: &TrixyConfig) -> TokenStream2 {
// Build the language wrappers
let c_host_api = c_api::host::generate(&trixy, &config);
// Build the final rust hosting code
let host_rust_code = host::generate(&trixy, &config);
quote! {
#host_rust_code
/* C api */
#c_host_api
}
}
fn identifier_to_rust(identifier: &Identifier) -> TokenStream2 {
let ident = format_ident!("{}", &identifier.name);
quote! {
#ident
}
}
fn named_type_to_rust(named_type: &NamedType) -> TokenStream2 {
let ident = identifier_to_rust(&named_type.name);
let r#type = type_to_rust(&named_type.r#type);
quote! {
#ident : #r#type
}
}
fn type_to_rust(r#type: &Type) -> TokenStream2 {
let ident = identifier_to_rust(&r#type.identifier);
if r#type.generic_args.is_empty() {
quote! {
#ident
}
} else {
let generics: Vec<TokenStream2> = r#type.generic_args.iter().map(type_to_rust).collect();
quote! {
#ident <#(#generics),*>
}
}
}

View File

@ -18,39 +18,91 @@
* If not, see <https://www.gnu.org/licenses/>. * If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::fs;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input; use std::{env, fs, io::Write, path::PathBuf, process::Command};
use trixy_lang_parser::parse_trixy_lang; use trixy_lang_parser::parse_trixy_lang;
use crate::config::TrixyConfig; use crate::config::TrixyConfig;
mod config; pub mod config;
mod generate; mod generate;
const VIM_LINE_RUST: &'static str = "// vim: filetype=rust\n";
const VIM_LINE_C: &'static str = "// vim: filetype=c\n";
impl TrixyConfig {
/// This is the heart of Trixy /// This is the heart of Trixy
/// It mainly does one thing: /// It mainly does one thing:
/// - Generate a tree of modules from the input trixy file /// - Generate a tree of modules from the input trixy file
/// ///
#[proc_macro] pub fn generate(&self) {
pub fn trixy_generate(input: TokenStream) -> TokenStream { let source_code = fs::read_to_string(&self.trixy_path).unwrap_or_else(|err| {
let input = parse_macro_input!(input as TrixyConfig); panic! {"Can't read file at path: '{}'. The Error is: '{}'",
self.trixy_path.display(), err};
let source_code = fs::read_to_string(input.get_path()).unwrap_or_else(|err| {
panic! {"Can't read file at path: '{}'. The Error is: '{}'", input.get_path().display(), err};
}); });
let trixy_code = parse_trixy_lang(&source_code).unwrap_or_else(|err| { let trixy_code = parse_trixy_lang(&source_code).unwrap_or_else(|err| {
panic! {"Parsing of the trixy file failed: \n{}", err} panic! {"Parsing of the trixy file failed: \n{}", err}
}); });
// Build the final rust hosting code // host code
let host_rust_code = generate::host::generate(&trixy_code, &input); let host_code = prettyplease::unparse(
&syn::parse2(generate::generate(&trixy_code, &self))
.expect("This code was generated, it should also be parsable"),
);
let mut host_code_out = fs::File::create(PathBuf::from(format!(
"{}/{}",
env::var("OUT_DIR").expect("The build script should have this define"),
&self.host_code_name.display()
)))
.expect("This file should always be free to use");
write!(host_code_out, "{}\n{}", host_code, VIM_LINE_RUST).expect("Write should work");
let output = quote! { // c header
#host_rust_code let c_header = generate::c_api::header::generate(&trixy_code, &self);
}; let c_header_path = PathBuf::from(format!(
output.into() "{}/{}",
env::var("OUT_DIR").expect("The build script should have this define"),
&self.c_header_name.display()
));
let mut c_header_out =
fs::File::create(&c_header_path).expect("This file should always be free to use");
write!(c_header_out, "{}\n{}", c_header, VIM_LINE_C).expect("Write should work");
Command::new("clang-format")
.args(["-i", &c_header_path.to_str().unwrap()])
.status()
.unwrap_or_else(|err| {
panic!(
"Failed to format the c header file with `clang-format`; Error: `{}`",
err
)
});
if let Some(dist_dir) = &self.dist_dir_path {
if !dist_dir.is_dir() {
fs::create_dir(dist_dir).unwrap_or_else(|err| {
panic! {
"Failed to create the dist directory ('{}') because of: `{}`",
dist_dir.display(), err}
});
}
if self.check_dist_dir {
if dist_dir.read_dir().iter().count() != 1 {
panic!("Your specified dist dir already has something in it! Set `check_dist_dir` to `false` to override this check");
}
}
let c_header_dist = PathBuf::from(format!(
"{}/{}",
dist_dir.display(),
self.c_header_name.display()
));
fs::copy(c_header_path, c_header_dist).unwrap_or_else(
|err| panic! {"Failed to copy the c header to the dist dir because of: `{}`", err},
);
}
}
} }