Files
GaCy-GO/docs/scratch/firefly_protoc_deep_dive.md
T

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)

  1. บทนำและวัตถุประสงค์ของโครงการ (Introduction & Problem Statement)
  2. สถาปัตยกรรมคอมไพเลอร์แบบ Multi-Tiered IR (Compiler Architecture)
    • 2.1 ภาพรวมของไปป์ไลน์การคอมไพล์ (Frontend \rightarrow IR \rightarrow Backend)
    • 2.2 โครงสร้างไวยากรณ์เสริม (Custom Syntax Extensions)
  3. ระบบคณิตศาสตร์เชิงสัญลักษณ์ (Symbolic Expression Inverter Engine)
    • 3.1 ปัญหาการพรางค่าข้อมูลบนสายส่ง (Wire Obfuscation / Masking)
    • 3.2 อัลกอริทึมการแปลงนิพจน์ผกผันพีชคณิตอัตโนมัติ (Algebraic Inversion)
  4. ระบบ Meta Fields และ Message Factory แบบ Zero-Reflection
    • 4.1 การฝัง Metadata (CmdId/Opcode) โดยไม่เสีย Overhead บนสายส่ง
    • 4.2 การสร้าง Compile-Time Factory Jump Table (IFactoryMessage)
  5. การสร้างโค้ด C# และการปรับปรุงประสิทธิภาพ (CodeGen & Performance Optimizations)
  6. รันไทม์ภายในของ Firefly (Firefly.Protobuf Runtime Architecture)
  7. ตัวอย่างจริงจากการคอมไพล์ (Real-World Compilation Walkthrough)
  8. การนำไปประยุกต์ใช้ในระบบเซิร์ฟเวอร์เกม (Integration with HSR/Genshin Private Servers)
  9. คู่มือวิศวกรรมสำหรับ 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:

  1. ไม่รองรับ Wire Data Masking / Obfuscation: เกมค่าย HoYoverse มักจะพรางค่าฟิลด์ตัวเลขบนสายส่ง (Wire Format) ด้วยการทำ XOR, บวก, ลบ หรือบิตชิฟต์ เช่น ค่า ID จริงคือ 1001 แต่ในสายส่งกลับถูกเข้ารหัสเป็น (1001 ^ 0x5A) + 7 คอมไพเลอร์ปกติของ Google ทำได้แค่อ่านค่าดิบ ทำให้วิศวกรต้องมาเขียนโค้ดแปลงค่าซ้ำซ้อนในทุก ๆ ฟิลด์
  2. ไม่มีแนวคิดเรื่อง Packet Opcode (CmdId) ในตัว: Protobuf มาตรฐานไม่มีช่องทางในการผูกหมายเลข Opcode เข้ากับ Class โดยตรง หากสร้างฟิลด์ธรรมดา ข้อมูล Opcode จะถูกส่งซ้ำซ้อนลงไปใน Payload สิ้นเปลืองแบนด์วิดท์
  3. การสร้างอินสแตนซ์พึ่งพา 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 หน้าที่ของแต่ละเลเยอร์ในไปป์ไลน์:

  1. AST (Abstract Syntax Tree):
    • แปลงไวยากรณ์ข้อความเป็นโครงสร้างข้อมูลเบื้องต้น (Decl.Message, Decl.Field, Decl.Enum, Expr.Binary)
  2. HIR (High-Level Intermediate Representation):
    • รวบรวม Scope และ Namespace ของไฟล์
    • จัดระเบียบความสัมพันธ์ของ Message ซ้อน Message (Nested Types)
    • สกัดนิพจน์ [get: ...] และ [set: ...] เข้าสู่ AttributeSet
  3. MIR (Mid-Level Intermediate Representation):
    • แมปประเภทข้อมูลของ Protobuf ไปเป็นประเภทข้อมูล C# (MirTypeRef)
    • แปลงชื่อแบบ snake_case เป็น PascalCase
    • รวบรวมฟิลด์ที่เป็น meta ที่ตรงกับคำสั่ง option message_factory เพื่อเตรียมสร้าง Factory Class
  4. LIR (Low-Level Intermediate Representation):
    • กำหนดโครงสร้างโค้ดระดับล่าง: Constructors, Property Getters/Setters, ฟิลด์ Backing Field (_Field), เมธอด WriteTo, MergeFrom, CalculateSize, Clone, และ Switch-Case ใน MessageFactory.Create
  5. CodeGen (C# Code Generator):
    • แปลง LIR ออกมาเป็นโค้ด C# ข้อความจริง โดยรันกระบวนการเขียนไฟล์แบบคู่ขนาน (Parallel.ForEach)

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 พิเศษเพื่อรีดประสิทธิภาพฮาร์ดแวร์สูงสุด:

  1. [MethodImpl(MethodImplOptions.AggressiveInlining)]:
    • บังคับให้ JIT และ NativeAOT นำโค้ดของเมธอด (เช่น Getter/Setter, Serialization routines) ไปวางแทนที่จุดเรียกโดยตรง (Inline Expansion) เพื่อตัดค่าใช้จ่ายในการ Call Stack ของ CPU
  2. [DebuggerNonUserCode]:
    • ป้องกันไม่ให้ Debugger กระโดดเข้าไปในโค้ดที่สร้างขึ้นโดยอัตโนมัติ เพิ่มความสะดวกในการดีบักระบบของผู้พัฒนา
  3. Partial Class Architecture (public partial class):
    • คลาสที่สร้างขึ้นทั้งหมดเป็น partial class ทำให้ผู้พัฒนาสามารถเขียนโค้ดส่วนขยาย (Extension Logic, Custom Operators, Helper Methods) แยกไฟล์ได้ โดยที่โค้ดจะไม่สูญหายเมื่อคอมไพล์ .proto ใหม่
  4. Zero-Allocation Stack Buffer ใน Varint Encoding:
    • เมธอดสำหรับเขียนตัวเลข Varint ใช้ Span<byte> buffer = stackalloc byte[10]; บน Stack Memory ล้วนๆ ไม่มีการจอง Heap ใดๆ

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 สำหรับฟิลด์ประเภท bytes
  • RepeatedField<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 ใช้งานฟิลด์ได้ตามปกติโดยไม่ต้องรู้สูตรพรางข้อมูล!
  1. ขจัด Boilerplate Code: เซิร์ฟเวอร์ไม่จำเป็นต้องมีโค้ดแปลงค่า (Converter Functions) นับร้อยจุด เพราะการพรางและถอดรหัสถูกฝังอยู่ในตัว Class ของ Protobuf โดยตรง
  2. ความเข้ากันได้กับโปรโตคอลหลายเวอร์ชัน: สามารถกำหนดสูตรการพรางค่าที่แตกต่างกันในแต่ละแพตช์ได้โดยเพียงแค่แก้สัญลักษณ์ในไฟล์ .proto
  3. การทำงานร่วมกับระบบ Router: สามารถเชื่อมโยง Opcode Dispatcher เข้ากับ MessageFactory.Create(cmdId) ได้ทันที

9. คู่มือวิศวกรรมสำหรับ AI ในการขยายขีดความสามารถ (AI Extension & Engineering Guide)

หากโมเดล AI อื่นต้องการปรับแต่งหรือขยายขีดความสามารถของ FireflyProtoc ต่อไป:

  1. ข้อควรระวังเรื่อง CLI Output Path:
    ใน src/FireflyProtoc.cs บรรทัดที่ 21 มีการฮาร์ดโค้ด $"./{outputFolder}"
    หากส่ง Path แบบ Absolute (เช่น C:\Users\...) จะทำให้ระบบพาธขัดข้อง (./C:\...)
    คำแนะนำในการแก้: เปลี่ยนให้ใช้ Path.GetFullPath(outputFolder) หรือตรวจสอบ Path.IsPathRooted(outputFolder) ก่อนต่อสตริง
  2. การคอมไพล์ในสภาพแวดล้อม .NET 10:
    ไฟล์ FireflyProtoc.csproj มีการเปิดแฟล็ก <PublishAot>true</PublishAot> สำหรับ net9.0 ซึ่งหากรันคำสั่ง dotnet run ภายใต้ .NET 10 SDK จะเกิดข้อผิดพลาดในการหา ILCompiler Targets
    คำแนะนำในการรัน: ให้ส่งแฟล็ก -c Release -p:PublishAot=false ขณะทดสอบ หรือปรับ <TargetFramework> ให้ตรงกับเวอร์ชันของ SDK ที่ติดตั้ง
  3. การเพิ่มตัวดำเนินการพรางข้อมูล (Bitshift, Modulo):
    หากต้องการให้รองรับการเลื่อนบิต (<<, >>) หรือเศษเหลือ (%) ให้ไปเพิ่ม Mapping ผกผันใน InvertWithRight และ InvertWithLeft ของ src/Semantic/ExpressionInverter.cs:
    • << ผกผันด้วย >>
    • >> ผกผันด้วย <<