26 KiB
รายงานการวิเคราะห์สถาปัตยกรรมและการทำงานของ FireflyProtoc (Deep-Dive Technical Specification)
เอกสารอ้างอิงทางเทคนิคระดับลึกสำหรับวิศวกรซอฟต์แวร์และโมเดลปัญญาประดิษฐ์ (AI Systems)
วิเคราะห์จากซอร์สโค้ดของโครงการ: gatokeeper/FireflyProtoc
ครอบคลุมสถาปัตยกรรมคอมไพเลอร์ Protobuf ยุคใหม่, การถอดรหัสความคลุมเครือของข้อมูล (Wire De-obfuscation), ระบบคณิตศาสตร์เชิงสัญลักษณ์ (Symbolic Expression Inverter), Meta Fields, Compile-Time Message Factory และรันไทม์ประสิทธิภาพสูง
สารบัญ (Table of Contents)
- บทนำและวัตถุประสงค์ของโครงการ (Introduction & Problem Statement)
- สถาปัตยกรรมคอมไพเลอร์แบบ Multi-Tiered IR (Compiler Architecture)
- 2.1 ภาพรวมของไปป์ไลน์การคอมไพล์ (Frontend
\rightarrowIR\rightarrowBackend) - 2.2 โครงสร้างไวยากรณ์เสริม (Custom Syntax Extensions)
- 2.1 ภาพรวมของไปป์ไลน์การคอมไพล์ (Frontend
- ระบบคณิตศาสตร์เชิงสัญลักษณ์ (Symbolic Expression Inverter Engine)
- 3.1 ปัญหาการพรางค่าข้อมูลบนสายส่ง (Wire Obfuscation / Masking)
- 3.2 อัลกอริทึมการแปลงนิพจน์ผกผันพีชคณิตอัตโนมัติ (Algebraic Inversion)
- ระบบ Meta Fields และ Message Factory แบบ Zero-Reflection
- 4.1 การฝัง Metadata (CmdId/Opcode) โดยไม่เสีย Overhead บนสายส่ง
- 4.2 การสร้าง Compile-Time Factory Jump Table (
IFactoryMessage)
- การสร้างโค้ด C# และการปรับปรุงประสิทธิภาพ (CodeGen & Performance Optimizations)
- รันไทม์ภายในของ Firefly (Firefly.Protobuf Runtime Architecture)
- ตัวอย่างจริงจากการคอมไพล์ (Real-World Compilation Walkthrough)
- การนำไปประยุกต์ใช้ในระบบเซิร์ฟเวอร์เกม (Integration with HSR/Genshin Private Servers)
- คู่มือวิศวกรรมสำหรับ AI ในการขยายขีดความสามารถ (AI Extension & Engineering Guide)
1. บทนำและวัตถุประสงค์ของโครงการ (Introduction & Problem Statement)
FireflyProtoc (พัฒนาโดย gatokeeper) คือคอมไพเลอร์ Protocol Buffers ทางเลือกที่ถูกออกแบบขึ้นมาโดยเฉพาะสำหรับ Protocol วิศวกรรมย้อนกลับ (Reverse Engineering) และ เซิร์ฟเวอร์จำลองเกม (Game Emulators) เช่น Honkai: Star Rail, Genshin Impact, และ Zenless Zone Zero
ข้อจำกัดของ protoc ดั้งเดิมของ Google:
- ไม่รองรับ Wire Data Masking / Obfuscation: เกมค่าย HoYoverse มักจะพรางค่าฟิลด์ตัวเลขบนสายส่ง (Wire Format) ด้วยการทำ XOR, บวก, ลบ หรือบิตชิฟต์ เช่น ค่า ID จริงคือ
1001แต่ในสายส่งกลับถูกเข้ารหัสเป็น(1001 ^ 0x5A) + 7คอมไพเลอร์ปกติของ Google ทำได้แค่อ่านค่าดิบ ทำให้วิศวกรต้องมาเขียนโค้ดแปลงค่าซ้ำซ้อนในทุก ๆ ฟิลด์ - ไม่มีแนวคิดเรื่อง Packet Opcode (CmdId) ในตัว: Protobuf มาตรฐานไม่มีช่องทางในการผูกหมายเลข Opcode เข้ากับ Class โดยตรง หากสร้างฟิลด์ธรรมดา ข้อมูล Opcode จะถูกส่งซ้ำซ้อนลงไปใน Payload สิ้นเปลืองแบนด์วิดท์
- การสร้างอินสแตนซ์พึ่งพา Reflection หรือ Dictionary: การแมป Opcode เป็น C# Object มักต้องใช้ Reflection หรือ Dictionary Lookup ขนาดใหญ่ ซึ่งช้าและไม่รองรับการทำ NativeAOT
FireflyProtoc แก้ปัญหาทั้งหมดนี้ที่ ระดับคอมไพเลอร์ (Compiler Level) ทำให้ไฟล์ .proto มีความสามารถในการแปลงค่าพรางข้อมูลอัตโนมัติและสร้าง O(1) Factory สำเร็จรูปทันที
2. สถาปัตยกรรมคอมไพเลอร์แบบ Multi-Tiered IR (Compiler Architecture)
FireflyProtoc ถูกสร้างขึ้นด้วยสถาปัตยกรรมคอมไพเลอร์ระดับมืออาชีพ โดยแบ่งขั้นตอนการทำงานออกเป็น 3 ชั้น Intermediate Representation (IR):
flowchart TD
ProtoFile[".proto Source Code<br/>(พร้อมไวยากรณ์เสริม meta, [get: ...])"] --> Lexer["Lexer (Token Stream)"]
Lexer --> Parser["Parser (AST: Node / Decl / Expr)"]
Parser --> TypeChecker["TypeChecker (Semantic Validation)"]
subgraph IR_Pipeline["Multi-Tiered IR Lowering"]
TypeChecker --> HIR["HIR Builder<br/>(High-Level IR: Messages, Enums, Scopes)"]
HIR --> MIR["MIR Builder<br/>(Mid-Level IR: Type Resolution, MessageFactory, PascalCase)"]
MIR --> LIR["LIR Builder<br/>(Low-Level IR: Classes, Methods, Body, Byte Stmts)"]
end
LIR --> CodeGen["CSharpGenerator (Parallel.ForEach File Emit)"]
CodeGen --> CSharpFiles["Generated High-Performance C# Files<br/>(Zero Reflection, AggressiveInlining, NativeAOT)"]
2.1 หน้าที่ของแต่ละเลเยอร์ในไปป์ไลน์:
- AST (Abstract Syntax Tree):
- แปลงไวยากรณ์ข้อความเป็นโครงสร้างข้อมูลเบื้องต้น (
Decl.Message,Decl.Field,Decl.Enum,Expr.Binary)
- แปลงไวยากรณ์ข้อความเป็นโครงสร้างข้อมูลเบื้องต้น (
- HIR (High-Level Intermediate Representation):
- รวบรวม Scope และ Namespace ของไฟล์
- จัดระเบียบความสัมพันธ์ของ Message ซ้อน Message (Nested Types)
- สกัดนิพจน์
[get: ...]และ[set: ...]เข้าสู่AttributeSet
- MIR (Mid-Level Intermediate Representation):
- แมปประเภทข้อมูลของ Protobuf ไปเป็นประเภทข้อมูล C# (
MirTypeRef) - แปลงชื่อแบบ snake_case เป็น PascalCase
- รวบรวมฟิลด์ที่เป็น
metaที่ตรงกับคำสั่งoption message_factoryเพื่อเตรียมสร้าง Factory Class
- แมปประเภทข้อมูลของ Protobuf ไปเป็นประเภทข้อมูล C# (
- LIR (Low-Level Intermediate Representation):
- กำหนดโครงสร้างโค้ดระดับล่าง: Constructors, Property Getters/Setters, ฟิลด์ Backing Field (
_Field), เมธอดWriteTo,MergeFrom,CalculateSize,Clone, และ Switch-Case ในMessageFactory.Create
- กำหนดโครงสร้างโค้ดระดับล่าง: Constructors, Property Getters/Setters, ฟิลด์ Backing Field (
- CodeGen (C# Code Generator):
- แปลง LIR ออกมาเป็นโค้ด C# ข้อความจริง โดยรันกระบวนการเขียนไฟล์แบบคู่ขนาน (
Parallel.ForEach)
- แปลง LIR ออกมาเป็นโค้ด C# ข้อความจริง โดยรันกระบวนการเขียนไฟล์แบบคู่ขนาน (
2.2 โครงสร้างไวยากรณ์เสริม (Custom Syntax Extensions)
| ไวยากรณ์ | รูปแบบการเขียน | ผลลัพธ์ที่คอมไพเลอร์สร้างให้ |
|---|---|---|
| Meta Field | meta int32 cmd_id = 1; |
สร้างค่าคงที่และ Getter ในคลาส แต่จะไม่ถูก Serialize ออกทางสายส่ง และสามารถใช้ Tag ชนกับฟิลด์ธรรมดาได้ |
| Masked Field | [get: (id ^ 1) + 2] int32 id = 3; |
ตัวแปร Id ภายนอกจะมองเห็นค่าจริง แต่ค่าที่ถูกส่ง/รับบนสายส่งจะผ่านสูตรคณิตศาสตร์ผกผัน |
| Message Factory | option message_factory = int32 cmd_id; |
สร้างคลาส MessageFactory พร้อมเมธอด Create(int cmd_id) คืนค่าอินสแตนซ์ของ Message นั้นทันที |
3. ระบบคณิตศาสตร์เชิงสัญลักษณ์ (Symbolic Expression Inverter Engine)
จุดที่อัจฉริยะที่สุดของ FireflyProtoc คือคลาส ExpressionInverter.cs ซึ่งทำหน้าที่เป็น Symbolic Algebra Solver ในตัวเอง
3.1 ปัญหาการพรางค่าข้อมูลบนสายส่ง (Wire Obfuscation / Masking)
สมมุติว่าในเกม ฟิลด์ hp มีสูตรการพรางข้อมูลทางสายส่งคือ:
\text{WireValue} = (\text{LogicalHP} \oplus 1) + 2
เมื่อเขียนโค้ดฝั่งอ่าน (Deserialization / MergeFrom), เราจำเป็นต้องถอดรหัสค่าจากสายส่งกลับมาเป็นค่าจริง:
\text{LogicalHP} = (\text{WireValue} - 2) \oplus 1
หากใช้คอมไพเลอร์ทั่วไป ผู้พัฒนาจะต้องมานั่งคำนวณและเขียนสูตรทั้งขาไป (get) และขากลับ (set) ด้วยตัวเอง หากมีฟิลด์แบบนี้เป็นร้อยๆ ฟิลด์ จะเกิดข้อผิดพลาดได้ง่ายมาก
3.2 อัลกอริทึมการแปลงนิพจน์ผกผันพีชคณิตอัตโนมัติ (Algebraic Inversion)
ใน FireflyProtoc ผู้ใช้สามารถเขียนเพียงขาเดียว:
[get: (id ^ 1) + 2] int32 id = 3;
ExpressionInverter จะทำการคลายโหนด (Unwind) ย้อนกลับจาก Root ของ AST Expression Tree:
flowchart TD
Input["Input: (id ^ 1) + 2"] --> Root["Root Node: Operator (+) with Constant (2)"]
Root --> InvertAdd["Invert (+) to (-)<br/>New Accumulator: (WireValue - 2)"]
InvertAdd --> NextNode["Next Node: Operator (^) with Constant (1)"]
NextNode --> InvertXor["Invert (^) to (^)<br/>Self-inverse Operator<br/>New Accumulator: ((WireValue - 2) ^ 1)"]
InvertXor --> ReachTarget["Reached Target Identifier 'id'"]
ReachTarget --> Output["Setter Expression: value - 2 ^ 1"]
ตารางการสลับตัวดำเนินการพีชคณิต (ExpressionInverter.cs):
- บวก (
+)\Longleftrightarrowลบ (-) - คูณ (
*)\Longleftrightarrowหาร (/) - บิตไวส์ XOR (
\oplus)\Longleftrightarrowบิตไวส์ XOR (\oplus) (XOR เป็น Self-Inverse Operator:A \oplus B \oplus B = A) - ลบเอกภาพ (Unary
-)\Longleftrightarrowลบเอกภาพ (Unary-)
4. ระบบ Meta Fields และ Message Factory แบบ Zero-Reflection
4.1 การฝัง Metadata (CmdId/Opcode) โดยไม่เสีย Overhead บนสายส่ง
ในไฟล์ .proto สามารถกำหนดฟิลด์เป็น meta:
message PlayerLoginCsReq {
meta int32 cmd_id = 11;
string token = 1;
}
เมื่อคอมไพล์:
cmd_idจะไม่ถูกนับรวมในCalculateSize()cmd_idจะไม่ถูกเขียนลงWriteTo(CodedOutputStream output)cmd_idจะไม่ถูกอ่านในMergeFrom(CodedInputStream input)- คลาสจะมีเมธอด:
public const int CmdId = 11; public int GetCmdId() => CmdId;
4.2 การสร้าง Compile-Time Factory Jump Table (IFactoryMessage)
เมื่อกำหนด option message_factory = int32 cmd_id; ที่หัวไฟล์ .proto
คอมไพเลอร์จะรวบรวมทุก Message ที่มีฟิลด์ meta int32 cmd_id = X; แล้วสร้างไฟล์ MessageFactory.cs:
public static class MessageFactory
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IFactoryMessage Create(int cmdId)
{
switch (cmdId)
{
case 11:
return new PlayerLoginCsReq();
case 12:
return new PlayerLoginScRsp();
default:
return null;
}
}
}
Tip
ความได้เปรียบระดับสูงด้านประสิทธิภาพ:
การสร้าง Switch-Case สำหรับ Type Instantiation ในขั้นตอน Compile-Time ทำให้คอมไพลเลอร์ C# และ JIT/AOT สามารถปรับให้เป็น Jump Table (O(1) Direct Branching) ในระดับ Assembly CPU Instruction ได้โดยตรง ปราศจากการทำ Boxing/Unboxing, แฮชแมป หรือ Reflection ใดๆ ทั้งสิ้น
5. การสร้างโค้ด C# และการปรับปรุงประสิทธิภาพ (CodeGen & Performance Optimizations)
โค้ด C# ที่ FireflyProtoc สร้างขึ้นมานั้น มีการใส่ Attribute พิเศษเพื่อรีดประสิทธิภาพฮาร์ดแวร์สูงสุด:
[MethodImpl(MethodImplOptions.AggressiveInlining)]:- บังคับให้ JIT และ NativeAOT นำโค้ดของเมธอด (เช่น Getter/Setter, Serialization routines) ไปวางแทนที่จุดเรียกโดยตรง (Inline Expansion) เพื่อตัดค่าใช้จ่ายในการ Call Stack ของ CPU
[DebuggerNonUserCode]:- ป้องกันไม่ให้ Debugger กระโดดเข้าไปในโค้ดที่สร้างขึ้นโดยอัตโนมัติ เพิ่มความสะดวกในการดีบักระบบของผู้พัฒนา
- Partial Class Architecture (
public partial class):- คลาสที่สร้างขึ้นทั้งหมดเป็น
partial classทำให้ผู้พัฒนาสามารถเขียนโค้ดส่วนขยาย (Extension Logic, Custom Operators, Helper Methods) แยกไฟล์ได้ โดยที่โค้ดจะไม่สูญหายเมื่อคอมไพล์.protoใหม่
- คลาสที่สร้างขึ้นทั้งหมดเป็น
- Zero-Allocation Stack Buffer ใน Varint Encoding:
- เมธอดสำหรับเขียนตัวเลข Varint ใช้
Span<byte> buffer = stackalloc byte[10];บน Stack Memory ล้วนๆ ไม่มีการจอง Heap ใดๆ
- เมธอดสำหรับเขียนตัวเลข Varint ใช้
6. รันไทม์ภายในของ Firefly (Firefly.Protobuf Runtime Architecture)
FireflyProtoc ไม่ได้พึ่งพาไลบรารี Google.Protobuf.dll แต่มาพร้อมรันไทม์ขนาดเล็กของตัวเองชื่อ Firefly.Protobuf ซึ่งอยู่ในโฟลเดอร์ runtime/:
classDiagram
class IMessage {
<<interface>>
+CalculateSize() int
+MergeFrom(CodedInputStream input)
+WriteTo(CodedOutputStream output)
+ToByteArray() byte[]
}
class IFactoryMessage {
<<interface>>
}
class CodedInputStream {
-byte[] m_Buffer
-int m_Position
+ReadTag() int
+ReadString() string
+ReadInt32() int
+ReadUInt32() uint
+ReadBytes() ByteString
}
class CodedOutputStream {
-Stream m_Stream
+WriteRawTag(int tag)
+WriteString(string value)
+WriteInt32(int value)
+WriteUInt32(uint value)
}
class MessageParser~T~ {
+ParseFrom(byte[] data) T
+ParseFrom(CodedInputStream input) T
}
IMessage <|.. IFactoryMessage
รายละเอียดส่วนประกอบหลักของรันไทม์:
CodedInputStream/CodedOutputStream: ควบคุมการอ่านและเขียนข้อมูลระดับบิตและไบต์ตามมาตรฐาน Wire Type (Varint, Fixed32, Fixed64, Length-Delimited)ByteString: ตัวแทนของ Immutable Byte Array สำหรับฟิลด์ประเภทbytesRepeatedField<T>: คอลเลกชันสำหรับฟิลด์ซ้ำ (repeated) ที่ปรับแต่งให้ขยายบัฟเฟอร์อย่างมีประสิทธิภาพMapField<TKey, TValue>: โครงสร้างข้อมูล Dictionary ประสิทธิภาพสูงสำหรับฟิลด์ประเภทmap<K, V>
7. ตัวอย่างจริงจากการคอมไพล์ (Real-World Compilation Walkthrough)
7.1 โค้ดต้นฉบับ Protobuf (example.proto):
syntax = "proto3";
option csharp_namespace = "FireflyProtoc.Example";
option message_factory = int32 department_id;
message Person {
meta int32 department_id = 1;
string name = 1;
string email = 2;
[get: (id ^ 1) + 2] int32 id = 3;
}
7.2 โค้ด C# ที่คอมไพล์ได้จริง (Person.cs):
// Generated by FireflyProtoc! do not modify!
#pragma warning disable CS8618
namespace FireflyProtoc.Example;
[global::System.Diagnostics.DebuggerNonUserCode]
public partial class Person : global::Firefly.Protobuf.IMessage<Person>, global::Firefly.Protobuf.IFactoryMessage
{
public const global::System.Int32 DepartmentId = 1;
public const global::System.Int32 NameFieldNumber = 1;
public const global::System.Int32 EmailFieldNumber = 2;
public const global::System.Int32 IdFieldNumber = 3;
private global::System.Int32 _Id;
[global::System.Diagnostics.DebuggerNonUserCode]
public global::System.Int32 Id
{
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
get
{
return (_Id ^ 1) + 2; // คำนวณค่าเพื่อส่งออก
}
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
set
{
_Id = value - 2 ^ 1; // คำนวณค่าย้อนกลับอัตโนมัติจากการ Invert นิพจน์!
}
}
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public global::System.Int32 GetDepartmentId()
{
return DepartmentId; // Meta Field คืนค่าได้ แต่ไม่ถูกเขียนลง Wire
}
public void WriteTo(global::Firefly.Protobuf.CodedOutputStream output)
{
if (Name != default)
{
output.WriteRawTag(10);
output.WriteString(_Name);
}
if (Email != default)
{
output.WriteRawTag(18);
output.WriteString(_Email);
}
if (Id != default)
{
output.WriteRawTag(24);
output.WriteInt32(_Id); // ส่งค่าดิบที่ผ่านการพรางแล้ว
}
}
}
8. การนำไปประยุกต์ใช้ในระบบเซิร์ฟเวอร์เกม (Integration with HSR/Genshin Private Servers)
ในสถาปัตยกรรมเซิร์ฟเวอร์ เช่น GaC-hkrpg-go หรือ March7thHoney:
sequenceDiagram
autonumber
actor Client as Game Client (Obfuscated Traffic)
participant KCP as Server KCP Layer
participant Factory as MessageFactory.Create(cmdId)
participant Proto as Generated IFactoryMessage
Client->>KCP: UDP Packet (CmdId: 11, Obfuscated Payload)
KCP->>Factory: MessageFactory.Create(11) [O(1) Switch Jump]
Factory-->>KCP: Return new PlayerGetTokenCsReq()
KCP->>Proto: message.MergeFrom(stream)
Note over Proto: ฟิลด์ที่พรางค่าจะถูกคำนวณย้อนกลับเป็นค่าจริงอัตโนมัติใน Setter!
KCP->>ServerHandler: Handler.Handle(session, message)
Note over ServerHandler: Server ใช้งานฟิลด์ได้ตามปกติโดยไม่ต้องรู้สูตรพรางข้อมูล!
- ขจัด Boilerplate Code: เซิร์ฟเวอร์ไม่จำเป็นต้องมีโค้ดแปลงค่า (Converter Functions) นับร้อยจุด เพราะการพรางและถอดรหัสถูกฝังอยู่ในตัว Class ของ Protobuf โดยตรง
- ความเข้ากันได้กับโปรโตคอลหลายเวอร์ชัน: สามารถกำหนดสูตรการพรางค่าที่แตกต่างกันในแต่ละแพตช์ได้โดยเพียงแค่แก้สัญลักษณ์ในไฟล์
.proto - การทำงานร่วมกับระบบ Router: สามารถเชื่อมโยง Opcode Dispatcher เข้ากับ
MessageFactory.Create(cmdId)ได้ทันที
9. คู่มือวิศวกรรมสำหรับ AI ในการขยายขีดความสามารถ (AI Extension & Engineering Guide)
หากโมเดล AI อื่นต้องการปรับแต่งหรือขยายขีดความสามารถของ FireflyProtoc ต่อไป:
- ข้อควรระวังเรื่อง CLI Output Path:
ในsrc/FireflyProtoc.csบรรทัดที่ 21 มีการฮาร์ดโค้ด$"./{outputFolder}"
หากส่ง Path แบบ Absolute (เช่นC:\Users\...) จะทำให้ระบบพาธขัดข้อง (./C:\...)
คำแนะนำในการแก้: เปลี่ยนให้ใช้Path.GetFullPath(outputFolder)หรือตรวจสอบPath.IsPathRooted(outputFolder)ก่อนต่อสตริง - การคอมไพล์ในสภาพแวดล้อม .NET 10:
ไฟล์FireflyProtoc.csprojมีการเปิดแฟล็ก<PublishAot>true</PublishAot>สำหรับnet9.0ซึ่งหากรันคำสั่งdotnet runภายใต้ .NET 10 SDK จะเกิดข้อผิดพลาดในการหา ILCompiler Targets
คำแนะนำในการรัน: ให้ส่งแฟล็ก-c Release -p:PublishAot=falseขณะทดสอบ หรือปรับ<TargetFramework>ให้ตรงกับเวอร์ชันของ SDK ที่ติดตั้ง - การเพิ่มตัวดำเนินการพรางข้อมูล (Bitshift, Modulo):
หากต้องการให้รองรับการเลื่อนบิต (<<,>>) หรือเศษเหลือ (%) ให้ไปเพิ่ม Mapping ผกผันในInvertWithRightและInvertWithLeftของsrc/Semantic/ExpressionInverter.cs:<<ผกผันด้วย>>>>ผกผันด้วย<<