Skip to content

Commit 6270c4e

Browse files
committed
feat: initial impls
0 parents  commit 6270c4e

15 files changed

Lines changed: 751 additions & 0 deletions

File tree

.github/workflows/check.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Check
2+
3+
on:
4+
push:
5+
branches:
6+
- slaveholder
7+
pull_request:
8+
branches:
9+
- slaveholder
10+
11+
env:
12+
CARGO_TERM_COLOR: always
13+
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v3
21+
22+
- name: Run cargo fmt
23+
run: cargo fmt --check --verbose
24+
25+
- name: Run cargo clippy
26+
run: cargo clippy --verbose
27+
28+
- name: Run tests
29+
run: cargo test --verbose

.github/workflows/publish.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Publish
2+
3+
on:
4+
push:
5+
tags:
6+
- v*
7+
8+
jobs:
9+
bump-publish:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v3
15+
16+
- name: Bump version
17+
run: 'cargo install vproj && vproj ${GITHUB_REF##*/} && git add . && git -c user.name="AlseinX" -c user.email="xyh951115@live.com" commit -am "chore: bump release version ${GITHUB_REF##*/}"'
18+
19+
- name: Run cargo fmt
20+
run: cargo fmt --check --verbose
21+
22+
- name: Run cargo clippy
23+
run: cargo clippy --verbose
24+
25+
- name: Run tests
26+
run: cargo test --verbose
27+
28+
- name: Publish
29+
id: publish-crates
30+
uses: katyo/publish-crates@v2
31+
with:
32+
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
33+
34+
- name: Publish Result
35+
if: fromJSON(steps.publish-crates.outputs.published).*
36+
run: |
37+
LIST="${{ join(fromJSON(steps.publish-crates.outputs.published).*.name, ', ') }}"
38+
echo "Published crates: $LIST"

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/target
2+
/Cargo.lock

Cargo.toml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[package]
2+
name = "ffi-enum"
3+
readme = "README.md"
4+
version.workspace = true
5+
description.workspace = true
6+
homepage.workspace = true
7+
repository.workspace = true
8+
documentation.workspace = true
9+
authors.workspace = true
10+
license.workspace = true
11+
edition.workspace = true
12+
keywords.workspace = true
13+
14+
[dependencies]
15+
macros = { package = "ffi-enum-macros", path = "macros" }
16+
17+
thiserror = "2"
18+
19+
[dev-dependencies]
20+
serde = { version = "1", features = ["derive"] }
21+
serde_json = { version = "1" }
22+
23+
[workspace]
24+
members = [".", "macros"]
25+
resolver = "2"
26+
27+
[workspace.package]
28+
version = "0.0.0"
29+
description = "Simply write and use `enum`s like rust native enums, freely passing through ffi"
30+
homepage = "https://github.com/AlseinX/ffi-enum"
31+
repository = "https://github.com/AlseinX/ffi-enum"
32+
documentation = "https://docs.rs/ffi-enum"
33+
authors = ["AlseinX <xyh951115@live.com>"]
34+
license = "MIT OR Apache-2.0"
35+
edition = "2021"
36+
keywords = ["ffi", "c-abi", "derive", "enum"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 AlseinX <xyh951115@live.com>
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.

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# ffi-enum
2+
3+
Simply write and use `enum`s like rust native enums, freely passing through ffi.
4+
5+
## Why not using `#[repr(C)]` and `#[non_exhaustive]` ?
6+
7+
+ Rust's `#[repr(C)]` is not fully equal to a C-abi `enum`, and it is still an **undefined behavior** when an `enum` defined with `#[repr(C)]` recieves a value that is not listed in the definition of `enum`, from ffi.
8+
+ `#[non_exhaustive]` is only designed for inter-crate compatibility, while the compiler might still optimize based on the assumption that the binary pattern of an `enum` value is always in its defined range.
9+
10+
## Why not existing alternatives?
11+
12+
The current alternatives mostly define complex DSL with function macros, which could be hard to read/maintain and not supported by `rustfmt` or `rust-analyzer`, and they do not support derive macros.
13+
14+
While this crate offers a nearly native experience by trying the best to fully mimic the behaviors of native `enums`, despite it requires non-exhaustive matching even inside the defining crate. FFi enums are defined with native rust enum item syntax with full support of formatting, code hint, and auto completion.
15+
16+
```rust
17+
use ffi_enum::{prelude::*, FfiEnum};
18+
use serde::{Deserialize, Serialize};
19+
20+
#[ffi_enum]
21+
#[derive(Debug, Serialize, Deserialize)]
22+
#[serde(rename_all = "snake_case")]
23+
pub enum Animal {
24+
Cat,
25+
Dog,
26+
}
27+
28+
fn main() {
29+
let json = serde_json::to_string(&Animal::Cat).unwrap();
30+
assert_eq!(json, "\"cat\"");
31+
let value: Animal = serde_json::from_str(&json).unwrap();
32+
assert_eq!(value, Animal::Cat);
33+
let json = serde_json::to_string(&Animal::from(100u8)).unwrap();
34+
assert_eq!(json, "\"<Unknown>\"");
35+
let value: Animal = serde_json::from_str(&json).unwrap();
36+
assert_eq!(value, Animal::UNKNOWN);
37+
}
38+
```

examples/derive_serde.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use ffi_enum::{prelude::*, FfiEnum};
2+
use serde::{Deserialize, Serialize};
3+
4+
#[ffi_enum]
5+
#[derive(Debug, Serialize, Deserialize)]
6+
#[serde(rename_all = "snake_case")]
7+
pub enum Animal {
8+
Cat,
9+
Dog,
10+
}
11+
12+
fn main() {
13+
let json = serde_json::to_string(&Animal::Cat).unwrap();
14+
assert_eq!(json, "\"cat\"");
15+
let value: Animal = serde_json::from_str(&json).unwrap();
16+
assert_eq!(value, Animal::Cat);
17+
let json = serde_json::to_string(&Animal::from(100u8)).unwrap();
18+
assert_eq!(json, "\"<Unknown>\"");
19+
let value: Animal = serde_json::from_str(&json).unwrap();
20+
assert_eq!(value, Animal::UNKNOWN);
21+
}

macros/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "ffi-enum-macros"
3+
readme = "README.md"
4+
version.workspace = true
5+
description.workspace = true
6+
homepage.workspace = true
7+
repository.workspace = true
8+
documentation.workspace = true
9+
authors.workspace = true
10+
license.workspace = true
11+
edition.workspace = true
12+
keywords.workspace = true
13+
14+
[lib]
15+
proc-macro = true
16+
17+
[dependencies]
18+
syn = { version = "2", features = ["full"] }
19+
quote = "1"
20+
proc-macro2 = "1"

macros/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# ffi-enum-macros
2+
3+
Do not directly use this proc-macro crate.
4+
5+
Take a look at [ffi-enum](https://crates.io/crates/ffi-enum)

macros/src/delegate.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use proc_macro2::{Span, TokenStream};
2+
use quote::quote_spanned;
3+
use syn::{
4+
Attribute, Expr, ExprLit, Ident, Lit, Meta, MetaNameValue, Path, PathArguments, PathSegment,
5+
Token, Type,
6+
};
7+
8+
use crate::utils::extract_meta_from_lists;
9+
10+
struct Context<'a, 'b> {
11+
name: &'a str,
12+
span: Span,
13+
target: &'a Type,
14+
origin: &'a Type,
15+
attrs: &'a [Attribute],
16+
impls: &'b mut TokenStream,
17+
}
18+
19+
impl Context<'_, '_> {
20+
fn debug(self) {
21+
let Self {
22+
span,
23+
target,
24+
origin,
25+
impls,
26+
..
27+
} = self;
28+
29+
impls.extend(quote_spanned! { span =>
30+
impl ::core::fmt::Debug for #target {
31+
#[inline(always)]
32+
fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
33+
if let Ok(origin) = <#origin as ::core::convert::TryFrom<#target>>::try_from(*self) {
34+
<#origin as ::core::fmt::Debug>::fmt(&origin, f)?;
35+
write!(f, "({})", self.repr)
36+
} else {
37+
write!(f, "<Unknown>({})", self.repr)
38+
}
39+
}
40+
}
41+
})
42+
}
43+
44+
fn serialize(self) {
45+
let Self {
46+
name,
47+
target,
48+
origin,
49+
span,
50+
attrs,
51+
impls,
52+
..
53+
} = self;
54+
55+
let serde = locate_serde(attrs);
56+
57+
impls.extend(quote_spanned! { span =>
58+
impl #serde::Serialize for #target {
59+
#[inline(always)]
60+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
61+
where
62+
S: #serde::Serializer,
63+
{
64+
if let Ok(origin) = <#origin as ::core::convert::TryFrom<#target>>::try_from(*self) {
65+
<#origin as #serde::Serialize>::serialize(&origin, serializer)
66+
} else {
67+
<S as #serde::Serializer>::serialize_unit_variant(serializer, #name, <#target as ::ffi_enum::FfiEnum>::UNKNOWN.repr as _, "<Unknown>")
68+
}
69+
}
70+
}
71+
});
72+
}
73+
74+
fn deserialize(self) {
75+
let Self {
76+
target,
77+
origin,
78+
span,
79+
attrs,
80+
impls,
81+
..
82+
} = self;
83+
let serde = locate_serde(attrs);
84+
85+
impls.extend(quote_spanned! { span =>
86+
impl<'de> #serde::Deserialize<'de> for #target {
87+
#[inline(always)]
88+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
89+
where
90+
D: #serde::Deserializer<'de>,
91+
{
92+
Ok(<#origin as #serde::Deserialize>::deserialize(deserializer).map(Into::into).unwrap_or_else(|_|<#target as ::ffi_enum::FfiEnum>::UNKNOWN))
93+
}
94+
}
95+
});
96+
}
97+
}
98+
99+
fn locate_serde(attrs: &[Attribute]) -> Path {
100+
extract_meta_from_lists(attrs, "serde")
101+
.find_map(|meta| {
102+
let MetaNameValue { path, value, .. } = meta.require_name_value().ok()?;
103+
if !path.is_ident("crate") {
104+
return None;
105+
}
106+
let Expr::Lit(ExprLit {
107+
lit: Lit::Str(s), ..
108+
}) = value
109+
else {
110+
return None;
111+
};
112+
s.parse::<Path>().ok()
113+
})
114+
.unwrap_or_else(|| Path {
115+
leading_colon: Some(Token![::](Span::mixed_site())),
116+
segments: [PathSegment {
117+
ident: Ident::new("serde", Span::mixed_site()),
118+
arguments: PathArguments::None,
119+
}]
120+
.into_iter()
121+
.collect(),
122+
})
123+
}
124+
125+
pub fn delegate<'a, 'b>(
126+
name: &'a str,
127+
target: &'a Type,
128+
origin: &'a Type,
129+
attrs: &'a [Attribute],
130+
impls: &'b mut TokenStream,
131+
) -> impl FnMut(Meta) + use<'a, 'b> {
132+
|meta| {
133+
let Meta::Path(path) = meta else {
134+
return;
135+
};
136+
137+
let Some(ident) = path.get_ident() else {
138+
return;
139+
};
140+
141+
let context = Context {
142+
name,
143+
span: ident.span().resolved_at(Span::mixed_site()),
144+
target,
145+
origin,
146+
attrs,
147+
impls,
148+
};
149+
150+
match ident.to_string().as_str() {
151+
"Debug" => context.debug(),
152+
"Serialize" => context.serialize(),
153+
"Deserialize" => context.deserialize(),
154+
_ => return,
155+
};
156+
}
157+
}

0 commit comments

Comments
 (0)