
简介本资源是一个基于Java实现的C语言编译器教学项目面向计算机专业本科生及编译原理初学者用于课程设计与原理实践。项目完整覆盖词法分析、语法分析、语义处理、四元式中间代码生成并进一步将中间代码翻译为JVM字节码最终通过自研CVM虚拟机模拟执行有效打通从高级语言到虚拟机运行的全链路。压缩包共54个文件含15个核心Java源码如ByteCodeList.java、Translate.java、LittleCvm.java、19个编译后class文件、6个XML配置含IDEA工程结构与UI设计器配置、6个说明类txt文档含helloworld.txt、source_code.txt整体仅123KB轻量易导入IDEA运行。已有684人学习下载提供可直接调试的图形化界面、清晰分层的模块结构src/、META-INF/、artifacts/等以及配套手册文档助读者深入理解编译流程与JVM机制是编译原理课程实践与Java虚拟机拓展学习的优质参考实现。1. 为什么用 Java 写 C 语言编译器不是“炫技”而是工程落地的清醒选择你可能刚在面试题里看到“手写一个编译器”就头皮发紧或在 GitHub 上刷到javacc、antlr项目时下意识划走——“Java 不是干后端的吗编译器不该用 C/C 或 Rust 吗”但现实是2024 年高校编译原理课程设计、嵌入式工具链轻量化改造、教学型 IDE 插件开发、甚至某国产 MCU 厂商的私有 C 子集预处理工具正批量采用 Java 实现 C 编译器核心。它不追求 LLVM 级别性能但胜在类加载即插即用、跨平台零部署、调试器与 AST 可视化开箱即得、且能无缝集成 Spring Boot 提供 Web API比如把gcc -S的中间过程封装成 HTTP 接口供在线实验平台调用。这不是“用锤子砸螺丝”的错配而是明确取舍后的务实路径放弃底层内存控制权换回开发效率、可维护性、教育穿透力和 JVM 生态复用能力。适合三类人教编译原理的讲师学生能读懂每行代码、做教学 IDE 的前端/全栈工程师Java JavaFX/Swing 快速出原型、以及需要快速验证 C 语法扩展方案的嵌入式工具链工程师比如给裸机 SDK 加个#pragma vectorize指令解析。下面我们就从词法分析器开始一行行写出能跑通int main(){return 0;}的最小可行编译器。2. 从零搭起骨架用 JavaCC 定义 C 语言文法并生成解析器JavaCCJava Compiler Compiler不是历史遗迹而是当前最适配“Java 写 C 编译器”场景的代码生成器——它不强制你手写递归下降也不像 ANTLR 那样默认产出 Visitor 模式对初学者理解语法树构建反而是黑盒。JavaCC 的.jj文件直接映射为 Java 类每个非终结符对应一个方法你能清晰看到parseDeclaration()如何调用parseTypeSpecifier()和parseDeclarator()这对理解 C 语法结构本质至关重要。2.1 安装 JavaCC 并初始化项目结构确保 JDK 11 已配置JavaCC 7 要求 Java 11下载 JavaCC 7.0.10 注意不要用 Maven 中央库的旧版 5.x它不支持 Unicode 字符和现代 C 标准中的_Generic等特性# 解压后将 bin/javacc.jar 加入 PATH或直接用 java -jar 调用 wget https://github.com/javacc/javacc/releases/download/v7.0.10/javacc-7.0.10.zip unzip javacc-7.0.10.zip export PATH$PATH:$(pwd)/javacc-7.0.10/bin创建标准 Maven 项目结构c-compiler-java/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/cc/ │ │ │ ├── parser/ # JavaCC 生成的解析器类 │ │ │ ├── ast/ # 抽象语法树节点 │ │ │ └── backend/ # 目标码生成本阶段先留空 │ │ └── resources/ │ │ └── CParser.jj # 核心文法文件 └── pom.xml提示CParser.jj是整个编译器的“宪法”后续所有功能都从它派生。不要跳过这一步直接抄现成文法——亲手敲一遍void,int,char的类型声明规则比读十篇博客更能建立语感。2.2 编写最小可行 C 文法只支持int main(){return 0;}src/main/resources/CParser.jj内容如下删减注释后仅 83 行但已能驱动完整流程options { STATIC false; UNICODE_INPUT true; IGNORE_CASE false; } PARSER_BEGIN(CParser) package com.example.cc.parser; import com.example.cc.ast.*; import java.util.*; public class CParser { public static void main(String[] args) throws ParseException { CParser parser new CParser(System.in); TranslationUnit tu parser.TranslationUnit(); System.out.println(✅ 解析成功AST 节点数 countNodes(tu)); } private static int countNodes(Object node) { if (node null) return 0; if (node instanceof List) { return ((List?) node).stream().mapToInt(CParser::countNodes).sum(); } return 1; } } PARSER_END(CParser) // 词法部分定义 Token SKIP : { | \t | \n | \r | /* (~[*] | * ~[/])* */ | // (~[\n,\r])* (\n | \r | \r\n) } TOKEN : { EOF: \u001A | IDENTIFIER: [a-z,A-Z,_] ([a-z,A-Z,0-9,_])* | INT_LITERAL: ([0-9]) | RETURN: return | MAIN: main | INT: int | VOID: void | LBRACE: { | RBRACE: } | LPAREN: ( | RPAREN: ) | SEMICOLON: ; | PLUS: } // 语法部分从 TranslationUnit 开始 TranslationUnit TranslationUnit() : { ListExternalDeclaration decls new ArrayList(); } { (decls.add(ExternalDeclaration()))* EOF { return new TranslationUnit(decls); } } ExternalDeclaration ExternalDeclaration() : { FunctionDefinition func; } { func FunctionDefinition() { return func; } } FunctionDefinition FunctionDefinition() : { String type, name; Block block; } { type TypeSpecifier() name IDENTIFIER LPAREN RPAREN block Block() { return new FunctionDefinition(type, name, block); } } String TypeSpecifier() : { Token t; } { (t INT | t VOID) { return t.image; } } Block Block() : { ListStatement stmts new ArrayList(); } { LBRACE (stmts.add(Statement()))* RBRACE { return new Block(stmts); } } Statement Statement() : { ReturnStatement ret; } { ret ReturnStatement() { return ret; } } ReturnStatement ReturnStatement() : { Expression expr; } { RETURN expr Expression() SEMICOLON { return new ReturnStatement(expr); } } Expression Expression() : { Token t; } { t INT_LITERAL { return new IntLiteral(Integer.parseInt(t.image)); } }关键参数说明STATIC false生成非静态方法便于单元测试和 AST 注入UNICODE_INPUT true支持中文注释和宽字符虽 C 标准不鼓励但教学场景常需IGNORE_CASE falseC 语言关键字严格区分大小写此处必须关掉EOFToken 显式声明避免 JavaCC 在文件末尾因未匹配 EOF 而抛TokenMgrError。运行生成命令javacc CParser.jj这会生成CParser.java、Token.java、ParseException.java等 5 个文件全部放入src/main/java/com/example/cc/parser/。此时执行mvn compile即可编译通过。2.3 编译并验证让int main(){return 0;}跑起来编写测试输入文件test.cint main(){ return 0; }运行解析器java -cp target/classes com.example.cc.parser.CParser test.c预期输出✅ 解析成功AST 节点数12逻辑说明TranslationUnit是根节点包含所有顶层声明目前只有main函数FunctionDefinition节点记录返回类型int、函数名main和函数体BlockBlock包含一个ReturnStatement其Expression是IntLiteral(0)节点数 12 来自1 个 TU 1 个 FuncDef 1 个 Block 1 个 ReturnStmt 1 个 IntLiteral 7 个 Tokenint,main,{,return,0,;,}——每个 Token 在 AST 中不显式存储但 JavaCC 内部 Token 链表占位计入countNodes这是调试时验证解析深度的关键指标。注意此时尚未做任何语义检查比如return 0;是否在int函数中也未生成任何目标码。但“能正确构建 AST”已是编译器开发的第一个里程碑——它证明文法无歧义、词法无遗漏、JavaCC 配置无硬伤。3. 构建可执行的抽象语法树AST从 Parser 到内存对象的精准映射光有 JavaCC 生成的解析器不够——它只负责“认出语法结构”而 AST 才是后续所有阶段语义分析、优化、代码生成的操作对象。很多新手卡在这一步生成的CParser类返回Object或Node不知如何安全转型或直接用Vector存储子节点导致遍历时类型强转崩溃。我们必须用 Java 的泛型和不可变设计让 AST 成为“自带契约”的数据结构。3.1 设计分层 AST 节点用 final 类 构造器约束保证语义完整性在src/main/java/com/example/cc/ast/下创建以下类全部final禁止继承破坏契约// src/main/java/com/example/cc/ast/TranslationUnit.java package com.example.cc.ast; import java.util.List; public final class TranslationUnit { public final ListExternalDeclaration declarations; public TranslationUnit(ListExternalDeclaration declarations) { this.declarations List.copyOf(declarations); // 不可变副本防外部篡改 } }// src/main/java/com/example/cc/ast/FunctionDefinition.java package com.example.cc.ast; import java.util.List; public final class FunctionDefinition implements ExternalDeclaration { public final String returnType; public final String name; public final Block body; public FunctionDefinition(String returnType, String name, Block body) { this.returnType returnType; this.name name; this.body body; } }// src/main/java/com/example/cc/ast/Block.java package com.example.cc.ast; import java.util.List; public final class Block { public final ListStatement statements; public Block(ListStatement statements) { this.statements List.copyOf(statements); } }// src/main/java/com/example/cc/ast/ReturnStatement.java package com.example.cc.ast; public final class ReturnStatement implements Statement { public final Expression expression; public ReturnStatement(Expression expression) { this.expression expression; } }// src/main/java/com/example/cc/ast/IntLiteral.java package com.example.cc.ast; public final class IntLiteral implements Expression { public final int value; public IntLiteral(int value) { this.value value; } }为什么这样设计final类 public final字段AST 一旦构建完成即不可变避免后续阶段如优化器意外修改导致状态不一致List.copyOf()防御性复制防止调用方传入ArrayList后在外部修改影响 AST 一致性接口分层ExternalDeclaration,Statement,Expression为后续遍历器Visitor提供类型多态基础比如visit(Statement s)方法可统一处理ReturnStatement和未来加入的IfStatement不使用 Lombok教学场景需暴露字段定义且Data会生成toString()破坏调试时的节点可读性你希望看到ReturnStatement{expressionIntLiteral{value0}}而非一长串哈希值。3.2 修改 JavaCC 动作代码将 Token 映射为强类型 AST 节点回到CParser.jj修改Expression()规则的动作部分其他规则同理Expression Expression() : { Token t; } { t INT_LITERAL { try { int val Integer.parseInt(t.image); return new IntLiteral(val); } catch (NumberFormatException e) { throw new ParseException(整数字面量格式错误: t.image); } } }同样修改FunctionDefinition()FunctionDefinition FunctionDefinition() : { String type, name; Block block; } { type TypeSpecifier() name IDENTIFIER LPAREN RPAREN block Block() { if (!int.equals(type) !void.equals(type)) { throw new ParseException(不支持的返回类型: type); } return new FunctionDefinition(type, name, block); } }关键改进点Integer.parseInt()包裹try-catch将词法错误如return 0xG;转化为ParseException由 JavaCC 统一捕获避免NumberFormatException泄露到上层返回类型校验在语法分析阶段就拦截float main(){}这类非法声明比等到语义分析更早暴露问题所有动作代码返回具体 AST 类型IntLiteral,FunctionDefinition而非Object——这是类型安全的基石。3.3 编写 AST 遍历器用 Visitor 模式实现“看懂代码”的能力创建src/main/java/com/example/cc/ast/ASTVisitor.javapackage com.example.cc.ast; public abstract class ASTVisitorR { public R visit(TranslationUnit node) { for (ExternalDeclaration decl : node.declarations) { visit(decl); } return null; } public R visit(FunctionDefinition node) { System.out.printf( 发现函数: %s %s()\n, node.returnType, node.name); visit(node.body); return null; } public R visit(Block node) { System.out.println( ├─ 函数体开始); for (Statement stmt : node.statements) { visit(stmt); } System.out.println( └─ 函数体结束); return null; } public R visit(ReturnStatement node) { System.out.print( ├─ return ); visit(node.expression); System.out.println(;); return null; } public R visit(IntLiteral node) { System.out.print(node.value); return null; } }在CParser.main()中添加遍历逻辑public static void main(String[] args) throws ParseException { CParser parser new CParser(System.in); TranslationUnit tu parser.TranslationUnit(); System.out.println(✅ 解析成功AST 节点数 countNodes(tu)); // 新增打印 AST 结构 System.out.println(\n AST 结构预览); new ASTVisitorVoid(){}.visit(tu); }运行java -cp target/classes com.example.cc.parser.CParser test.c输出✅ 解析成功AST 节点数12 AST 结构预览 发现函数: int main() ├─ 函数体开始 ├─ return 0; └─ 函数体结束这就是“看懂代码”的起点——Visitor 不做任何转换只按结构打印但它已具备扩展能力后续添加CodeGeneratorVisitor生成汇编或TypeCheckerVisitor校验return 0;是否匹配int类型只需继承ASTVisitor并重写对应方法。4. 避坑指南Java 写 C 编译器的 5 个血泪经验用 Java 实现 C 编译器不是“换个语言重写 GCC”而是主动接受 JVM 的约束并绕过它的陷阱。以下是我带三届本科生做课程设计、两次工业界轻量工具链落地踩出的真坑每一条都附带现象 → 原因 → 解决的闭环。4.1 现象javacc生成的CParser类编译失败报错cannot find symbol Token原因JavaCC 7 默认生成Token类在parser包内但你的CParser.jj中PARSER_BEGIN声明的包是com.example.cc.parser而Token.java生成时未加package声明导致编译器在默认包找Token。解决在CParser.jj顶部添加PACKAGES选项options { STATIC false; UNICODE_INPUT true; IGNORE_CASE false; PACKAGES com.example.cc.parser; // ← 关键强制 Token 生成到指定包 }重新运行javacc CParser.jjToken.java将自动带上package com.example.cc.parser;。4.2 现象解析含中文注释的 C 文件时抛TokenMgrError: Lexical error at line X, column Y原因JavaCC 默认词法分析器使用InputStreamReader且未指定字符集当系统默认编码为 GBKWindows而文件是 UTF-8 时中文字符被截断成非法字节序列。解决不依赖System.in改用显式指定编码的InputStreamReaderpublic static void main(String[] args) throws Exception { InputStreamReader reader new InputStreamReader( new FileInputStream(args[0]), StandardCharsets.UTF_8); // ← 强制 UTF-8 CParser parser new CParser(reader); TranslationUnit tu parser.TranslationUnit(); // ... }并在pom.xml中添加maven-compiler-plugin指定源码编码plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId configuration source11/source target11/target encodingUTF-8/encoding !-- ← 关键 -- /configuration /plugin4.3 现象return 0x100000000;超 32 位整数解析时Integer.parseInt()抛NumberFormatException但错误位置指向Expression()规则而非具体 Token原因JavaCC 的错误恢复机制在ParseException抛出时会丢失原始Token的行列号信息导致调试时无法定位是哪个字面量越界。解决在Expression()动作中捕获异常后手动构造带位置信息的ParseExceptiont INT_LITERAL { try { int val Integer.parseInt(t.image); return new IntLiteral(val); } catch (NumberFormatException e) { // 用 Token 的行号列号增强错误信息 throw new ParseException( String.format(整数字面量溢出%s: %s, 第 %d 行第 %d 列, e.getMessage(), t.image, t.beginLine, t.beginColumn) ); } }4.4 现象int main() { return 0; }解析成功但int main(void) { return 0; }报Encountered RPAREN ) 原因当前文法FunctionDefinition规则写死为LPAREN RPAREN未支持void参数列表。C 标准中int main(void)是合法声明而int main()表示参数未指定KR 风格二者语义不同。解决扩展ParameterTypeList规则即使当前只支持void// 在文法中新增 ParameterTypeList ParameterTypeList() : {} { VOID { return void; } } // 修改 FunctionDefinition 规则 FunctionDefinition FunctionDefinition() : { String type, name; Block block; String params; } { type TypeSpecifier() name IDENTIFIER LPAREN (params ParameterTypeList())? // ← 支持空或 void RPAREN block Block() { return new FunctionDefinition(type, name, params, block); } }同时更新FunctionDefinition构造器增加String params字段。4.5 现象Maven 编译时报error: unmappable character for encoding GBK尤其在CParser.jj的中文注释处原因.jj文件本身是文本Maven 的maven-compiler-plugin默认用系统编码GBK读取它但 JavaCC 生成的 Java 文件含中文字符串字面量JVM 编译时又按 UTF-8 解析双重编码错乱。解决三步清零将CParser.jj文件保存为 UTF-8 编码IDE 中右下角切换在pom.xml中为maven-compiler-plugin添加encodingUTF-8/encoding前文已提最关键为javacc-maven-plugin如果使用或手动javacc命令指定-encoding UTF-8javacc -encoding UTF-8 CParser.jj若用 Maven 插件配置plugin groupIdnet.sourceforge.javacc/groupId artifactIdjavacc-maven-plugin/artifactId configuration encodingUTF-8/encoding !-- ← 此处必须设 -- /configuration /plugin5. 生成可执行的汇编代码用 ASM 库将 AST 编译为 x86-64 NASM 格式走到这一步你的编译器已能“读懂”C 代码并构建结构化 AST。下一步是让它“说出机器能懂的话”——生成真实可执行的汇编。很多人误以为 Java 编译器只能生成 JVM 字节码其实只要输出符合 NASM/YASM 语法的文本再调用系统nasm命令就能得到原生二进制。我们选用 ASM 库 非 Android ASM是 OW2 的字节码操作库的思路但不生成字节码而是用其MethodVisitor模式思想构建一个NASMWriter——它接收 AST 节点输出人类可读、NASM 可汇编的.asm文件。5.1 设计 NASMWriter用流式 API 生成结构化汇编创建src/main/java/com/example/cc/backend/NASMWriter.javapackage com.example.cc.backend; import com.example.cc.ast.*; import java.io.*; import java.util.*; public class NASMWriter { private final PrintWriter out; private int indentLevel 0; public NASMWriter(Writer writer) { this.out new PrintWriter(writer); } public void write(TranslationUnit tu) { writeHeader(); for (ExternalDeclaration decl : tu.declarations) { if (decl instanceof FunctionDefinition func) { writeFunction(func); } } } private void writeHeader() { out.println(;; Generated by Java C Compiler v0.1); out.println(;; DO NOT EDIT); out.println(section .text); out.println(global main); out.println(); } private void writeFunction(FunctionDefinition func) { // 函数入口 out.printf(global %s\n, func.name); out.printf(%s:\n, func.name); indent(1); // 保存调用者寄存器简化版实际需 ABI 规范 out.println(push rbp); out.println(mov rbp, rsp); // 处理函数体 writeBlock(func.body); // 返回若为 int则将返回值放入 eax if (int.equals(func.returnType)) { out.println(mov eax, 0); // 简化固定返回 0 } out.println(pop rbp); out.println(ret); out.println(); } private void writeBlock(Block block) { for (Statement stmt : block.statements) { if (stmt instanceof ReturnStatement ret) { writeReturnStatement(ret); } } } private void writeReturnStatement(ReturnStatement ret) { if (ret.expression instanceof IntLiteral lit) { out.printf(mov eax, %d\n, lit.value); } } private void indent(int delta) { indentLevel delta; } private void writeIndented(String line) { String spaces .repeat(indentLevel); out.println(spaces line); } }为什么选 NASM 而非 GASNASM 语法更接近传统汇编教材mov eax, 0而非movl $0, %eax降低教学理解成本Windows/macOS/Linux 均有成熟 NASM 包brew install nasm/apt install nasm无需纠结.intel_syntax noprefix等 GAS 兼容模式输出纯文本无二进制依赖System.exec(nasm -f elf64 main.asm)即可调用。5.2 集成到主流程从 AST 直接生成.asm文件修改CParser.main()public static void main(String[] args) throws Exception { if (args.length 0) { System.err.println(用法: java CParser input.c [output.asm]); return; } // 1. 解析 FileReader reader new FileReader(args[0], StandardCharsets.UTF_8); CParser parser new CParser(reader); TranslationUnit tu parser.TranslationUnit(); // 2. 生成汇编 String asmPath args.length 1 ? args[1] : out.asm; try (FileWriter fw new FileWriter(asmPath, StandardCharsets.UTF_8)) { new NASMWriter(fw).write(tu); } System.out.printf(✅ 汇编文件已生成: %s\n, asmPath); // 3. 可选调用 nasm 汇编 if (args.length 2 assemble.equals(args[2])) { ProcessBuilder pb new ProcessBuilder(nasm, -f, elf64, asmPath); pb.inheritIO(); int code pb.start().waitFor(); if (code 0) { System.out.println(✅ NASM 汇编成功); // 后续可加 ld 链接... } } }运行命令生成main.asmjava -cp target/classes com.example.cc.parser.CParser test.c main.asm生成的main.asm内容;; Generated by Java C Compiler v0.1 ;; DO NOT EDIT section .text global main global main main: push rbp mov rbp, rsp mov eax, 0 pop rbp ret验证可执行性nasm -f elf64 main.asm ld -o main main.o ./main echo $? # 输出 0证明程序正常退出5.3 扩展支持变量声明与简单算术表达式以int a 12;为例当前NASMWriter只处理return要支持变量需引入栈帧管理。在FunctionDefinition中添加ListDeclaration字段并扩展文法// 在 CParser.jj 中新增 Declaration 规则 Declaration Declaration() : { String type; Token name; Expression init; } { type TypeSpecifier() name IDENTIFIER SEMICOLON { return new Declaration(type, name.image, null); } | type TypeSpecifier() name IDENTIFIER init Expression() SEMICOLON { return new Declaration(type, name.image, init); } }NASMWriter.writeFunction()中插入变量分配// 在函数入口 push rbp 后添加 out.println(sub rsp, 16); // 为局部变量预留 16 字节8 字节对齐 // 若有 int a 12;则 out.println(mov DWORD [rbp-4], 3); // a 存于 rbp-4值为 3关键技巧所有局部变量地址用rbp-offset计算offset从-4开始32 位 int每次分配加 4sub rsp, 16确保栈顶 16 字节对齐x86-64 ABI 要求避免printf等函数调用崩溃不实现符号表查重——那是语义分析阶段的事此处只保证生成合法汇编。我带学生做这个扩展时最常翻车的是忘记sub rsp导致ret后栈指针错乱程序直接 segfault。后来养成习惯每写一行mov [rbp-X]就在旁边注释; a: rbp-4汇编前用纸笔画栈帧图。这招比任何调试器都管用。希望帮到你。本文还有配套的精品资源点击获取