Skip to main content

libperl_macrogen/
lib.rs

1//! # libperl-macrogen
2//!
3//! Generate Rust FFI bindings for the things `bindgen` can't see:
4//! C **macro functions** and **`static inline`** definitions in
5//! Perl's header tree.
6//!
7//! `rust-bindgen` is the standard for translating C declarations to
8//! Rust, but it deliberately skips macro-shaped function definitions
9//! (because they have no fixed type signature) and produces no Rust
10//! body for `static inline` functions (because their definitions live
11//! in headers, not the linked library). For wrapping libperl that
12//! gap is huge — much of the public-looking API (`SvIV`, `newRV_inc`,
13//! `PL_stack_base`, hundreds more) is exposed as macros or
14//! `static inline` only.
15//!
16//! `libperl-macrogen` complements `bindgen`: it lex / parse /
17//! type-infers the relevant headers and emits Rust wrappers like
18//!
19//! ```text
20//! pub unsafe fn SvIV(my_perl: *mut PerlInterpreter, sv: *mut SV) -> IV {
21//!     unsafe { Perl_SvIV(my_perl, sv) }
22//! }
23//! ```
24//!
25//! plus declarative macros for `PERLVAR`-driven globals (so the same
26//! `PL_stack_base!(my_perl)` source compiles against threaded and
27//! non-threaded Perl).
28//!
29//! ## Library API
30//!
31//! The high-level entry point is the [`Pipeline`] builder, which
32//! drives a header file through the preprocess → infer → codegen
33//! stages and writes a Rust source file:
34//!
35//! ```no_run
36//! use libperl_macrogen::Pipeline;
37//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
38//! let mut output = std::fs::File::create("macro_bindings.rs")?;
39//! Pipeline::builder("xs-wrapper.h")
40//!     .with_auto_perl_config()?
41//!     .with_bindings("bindings.rs")     // bindgen output for type info
42//!     .with_codegen_defaults()
43//!     .build()?
44//!     .generate(&mut output)?;
45//! # Ok(())
46//! # }
47//! ```
48//!
49//! See [`PipelineBuilder`] for the full set of options
50//! (skip-list, extra include paths, codegen knobs, ...).
51//!
52//! ## CLI
53//!
54//! Installing the crate also gives you a `libperl-macrogen` binary
55//! for one-off / inspection use. Run with `--help` for the option
56//! summary.
57//!
58//! ## Apidoc data
59//!
60//! The crate bundles a pre-extracted snapshot of perlapi documentation
61//! (`apidoc.tar.gz`, ~1.9 MiB compressed) that the type inferencer
62//! consults during macro-wrapper generation. This means **no network
63//! access is required at build time** — works under docs.rs's
64//! `--network none` sandbox, in air-gapped CI, etc.
65//!
66//! For advanced use (e.g. testing an unreleased apidoc dataset on an
67//! offline mirror), set the `LIBPERL_APIDOC_URL` environment variable
68//! to override and download from there instead.
69//!
70//! ## Status
71//!
72//! Pre-1.0 — focused on the libperl-rs use case. Wider header-tree
73//! coverage and stable APIs come after libperl-rs hits 1.0.
74
75pub mod apidoc;
76pub mod apidoc_data;
77pub mod apidoc_patches;
78pub mod ast;
79pub mod c_fn_decl;
80pub mod error;
81pub mod enum_dict;
82pub mod fields_dict;
83pub mod global_const_dict;
84pub mod infer_api;
85pub mod inline_fn;
86pub mod intern;
87pub mod lexer;
88pub mod macro_def;
89pub mod macro_infer;
90pub mod parser;
91pub mod perl_config;
92pub mod perlvar_dict;
93pub mod perlvar_emitter;
94pub mod pipeline;
95pub mod pp_expr;
96pub mod preprocessor;
97pub mod rust_codegen;
98pub mod rust_decl;
99pub mod static_array_emitter;
100pub mod struct_emitter;
101pub mod semantic;
102pub mod sexp;
103pub mod syn_codegen;
104pub mod source;
105pub mod token;
106pub mod token_source;
107pub mod type_env;
108pub mod type_repr;
109pub mod local_usage;
110pub mod unified_type;
111
112/// この crate 自身のバージョン。下流 (libperl-sys) の regen ガードが
113/// apidoc data version と組にしてスタンプ比較することで、codegen-only
114/// リリースでも確実に再生成させるために公開する (GH #18)。
115pub const VERSION: &str = env!("CARGO_PKG_VERSION");
116
117// 主要な型を再エクスポート
118pub use apidoc::{
119    find_apidoc_dir_from, resolve_apidoc_path,
120    ApidocArg, ApidocCollector, ApidocDict, ApidocEntry, ApidocFlags, ApidocResolveError, ApidocStats, Nullability,
121};
122pub use infer_api::{
123    run_inference_with_preprocessor,
124    DebugOptions, InferConfig, InferError, InferResult, InferStats, TypedefDict,
125};
126pub use ast::*;
127pub use error::{CompileError, DisplayLocation, LexError, PPError, ParseError, Result};
128pub use fields_dict::FieldsDict;
129pub use inline_fn::InlineFnDict;
130pub use intern::{InternedStr, StringInterner};
131pub use rust_decl::RustDeclDict;
132pub use lexer::{IdentResolver, Interning, Lexer, LookupOnly, MutableLexer, ReadOnlyLexer};
133pub use macro_def::{MacroDef, MacroKind, MacroTable};
134pub use macro_infer::{
135    convert_assert_calls_in_compound_stmt, detect_assert_kind, InferStatus, MacroInferContext,
136    MacroInferInfo, MacroInferStats, NoExpandSymbols, ParseResult,
137};
138pub use parser::{parse_expression_from_tokens, parse_expression_from_tokens_ref, parse_type_from_string, Parser};
139pub use perl_config::{
140    build_pp_config_for_perl, get_default_target_dir, get_perl_config, get_perl_version,
141    PerlConfig, PerlConfigError,
142};
143pub use perlvar_dict::{
144    ArrayLength, PerlvarCollector, PerlvarDict, PerlvarEntry, PerlvarKind,
145};
146pub use preprocessor::{
147    CallbackPair, CommentCallback, MacroCalledCallback, MacroCallWatcher, MacroDefCallback,
148    PPConfig, Preprocessor,
149};
150pub use semantic::{SemanticAnalyzer, Symbol, SymbolKind, Type};
151pub use sexp::{SexpPrinter, TypedSexpPrinter};
152pub use source::{FileId, FileRegistry, SourceLocation};
153pub use token::{Comment, CommentKind, Token, TokenKind};
154pub use token_source::{TokenSlice, TokenSliceRef, TokenSource};
155pub use type_env::{ParamLink, TypeConstraint, TypeEnv};
156pub use type_repr::{
157    CDerivedType, CPrimitiveKind, CTypeSource, CTypeSpecs, InferredType,
158    IntSize as TypeReprIntSize, RustPrimitiveKind, RustTypeRepr, RustTypeSource, TypeRepr,
159};
160pub use unified_type::{IntSize, SourcedType, TypeSource, UnifiedType};
161pub use rust_codegen::{CodegenConfig, CodegenDriver, CodegenStats, GeneratedCode, GenerateStatus, RustCodegen};
162pub use rust_codegen::{CodegenReport, RequireCodegenError, RequireViolation};
163pub use pipeline::{
164    Pipeline, PipelineBuilder, PipelineError,
165    PreprocessConfig, InferConfig as PipelineInferConfig, CodegenConfig as PipelineCodegenConfig,
166    PreprocessedPipeline, InferredPipeline, GeneratedPipeline,
167};
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use std::path::PathBuf;
173
174    #[test]
175    fn test_basic_lexer_integration() {
176        let source = b"int main(void) { return 0; }";
177
178        let mut files = FileRegistry::new();
179        let file_id = files.register(PathBuf::from("test.c"));
180
181        let mut interner = StringInterner::new();
182        let mut lexer = Lexer::new(source, file_id, &mut interner);
183
184        let mut tokens = Vec::new();
185        loop {
186            let token = lexer.next_token().unwrap();
187            if matches!(token.kind, TokenKind::Eof) {
188                break;
189            }
190            tokens.push(token);
191        }
192
193        // int main ( void ) { return 0 ; }
194        assert_eq!(tokens.len(), 10);
195        // キーワードはキーワードトークンとして返される
196        assert!(matches!(tokens[0].kind, TokenKind::KwInt));
197        assert!(matches!(tokens[1].kind, TokenKind::Ident(_)));  // main is identifier
198        assert!(matches!(tokens[2].kind, TokenKind::LParen));
199        assert!(matches!(tokens[3].kind, TokenKind::KwVoid));
200        assert!(matches!(tokens[4].kind, TokenKind::RParen));
201        assert!(matches!(tokens[5].kind, TokenKind::LBrace));
202        assert!(matches!(tokens[6].kind, TokenKind::KwReturn));
203        assert!(matches!(tokens[7].kind, TokenKind::IntLit(0)));
204        assert!(matches!(tokens[8].kind, TokenKind::Semi));
205        assert!(matches!(tokens[9].kind, TokenKind::RBrace));
206
207        // 識別子の内容を確認
208        if let TokenKind::Ident(id) = tokens[1].kind {
209            assert_eq!(interner.get(id), "main");
210        } else {
211            panic!("Expected identifier for 'main'");
212        }
213    }
214
215    #[test]
216    fn test_comment_preservation() {
217        let source = b"// doc comment\nint x;";
218
219        let mut files = FileRegistry::new();
220        let file_id = files.register(PathBuf::from("test.c"));
221
222        let mut interner = StringInterner::new();
223        let mut lexer = Lexer::new(source, file_id, &mut interner);
224
225        // 最初に改行トークンが来る(コメントはその前)
226        let newline = lexer.next_token().unwrap();
227        assert!(matches!(newline.kind, TokenKind::Newline));
228        assert_eq!(newline.leading_comments.len(), 1);
229        assert!(newline.leading_comments[0].text.contains("doc comment"));
230
231        let token = lexer.next_token().unwrap();
232        // キーワードはキーワードトークンとして返される
233        assert!(matches!(token.kind, TokenKind::KwInt));
234    }
235}