[C++ / Python / DB] ProcedureGenerator

2023. 9. 1. 22:31·Study/C++ & C#

이전에 Protobuf를 사용한 패킷 핸들러를 자동화할 때와 동일하게,

Python과 Jinja2를 활용할 것이다.

 

1. XML 파서 작성

서버 소스가 아니라 Python에서 사용할 XML 파서를 작성해야 한다.

 

import xml.etree.ElementTree as ET

class XmlDBParser:
    def __init__(self):
        self.tables = {}
        self.procedures = []

    def parse_xml(self, path):
        tree = ET.parse(path)
        root = tree.getroot()
        for child in root:
            if child.tag == 'Table':
                 self.tables[child.attrib['name']] = Table(child)
        for child in root:
            if child.tag == 'Procedure':
                self.procedures.append(Procedure(child, self.tables))

# 적당히 필요한 정보만 뽑는다
# 테이블에 대해 모든 것을 알 필요는 없음!
class Table:
    def __init__(self, node):
        self.name = node.attrib['name']
        self.columns = {}
        for child in node:
            if child.tag == 'Column':
                self.columns[child.attrib['name']] = ReplaceType(child.attrib['type'])

class Procedure:
    def __init__(self, node, tables):
        name = node.attrib['name']
        if name.startswith('sp'):
            self.name = name[2:]
        else:
            self.name = name
        self.params = []
        for child in node:
            if child.tag == 'Param':
                self.params.append(Param(child))
            elif child.tag == 'Body':
                self.columns = ParseColumns(child, tables)
                self.questions = MakeQuestions(self.params)

class Param: # ...

class Column: # ...

# SELECT에 뭐가 있는지, 그 개수는 몇개인지 등을 알아야 한다.
# 몇개의 정보를 가져와야 하고, 인자로는 어떤게 있으며 어떤 테이블이 대상인지...
# 이런 데이터를 관리하기 위함
def ParseColumns(node, tables):
    columns = []
    query = node.text
    select_idx = max(query.rfind('SELECT'), query.rfind('select'))
    from_idx = max(query.rfind('FROM'), query.rfind('from'))
    if select_idx > 0 and from_idx > 0 and from_idx > select_idx:
        table_name = query[from_idx+len('FROM') : -1].strip().split()[0]
        table_name = table_name.replace('[', '').replace(']', '').replace('dbo.', '')
        table = tables.get(table_name)
        words = query[select_idx+len('SELECT') : from_idx].strip().split(",")
        for word in words:
            column_name = word.strip().split()[0]
            columns.append(Column(column_name, table.columns[column_name]))
    elif select_idx > 0:
        word = query[select_idx+len('SELECT') : -1].strip().split()[0]
        if word.startswith('@@ROWCOUNT') or word.startswith('@@rowcount'):
            columns.append(Column('RowCount', 'int64'))
        elif word.startswith('@@IDENTITY') or word.startswith('@@identity'):
            columns.append(Column('Identity', 'int64'))
    return columns

# 인자를 넣기 위한 ? 부분
def MakeQuestions(params): #...

def ReplaceType(type):
    if type == 'bool':
        return 'bool'
    if type == 'int':
        return 'int32'
    if type == 'bigint':
        return 'int64'
    if type == 'datetime':
        return 'TIMESTAMP_STRUCT'
    if type.startswith('nvarchar'):
        return 'nvarchar'
    return type

Python에서 XML을 사용하기 위해 xml.etree.ElementTree를 Import 해야 한다.

 

전체적인 흐름은 아래와 같다.

  1. 경로를 넘겨받음
  2. parse_xml() 호출
  3. 노드를 순회하며 Table 및 Procedure를 파싱해 저장
     - 각 Procedure의 Body를 파싱 하기 위해
     - ParseColumns() 및 MakeQuestions() 호출
  4. 외부에서 XML로부터 파싱 된 데이터 사용

길이가 길어서 좀 쫄긴 했는데....

코드 자체는 파이썬 코드를 대충이라도 볼 줄 안다면 다 보이는 것들이긴 하다.

 

 

2. 템플릿

패킷 뽑던 때와는 조금 다르다.

 

#pragma once
#include "Types.h"
#include <windows.h>
#include "DBBind.h"

{%- macro gen_procedures() -%} {% include 'Procedure.h' %} {% endmacro %}

namespace SP
{
	{{gen_procedures() | indent}}
};

템플릿 안에 또 다른 템플릿이 있는 구조다.

부분으로 나누어 작성의 효율성을 높이고 가독성의 향상을 노릴 수 있다.

 

실제로 내용물이 담기는 부분은 아래와 같다.

{%- macro lower_first(text) %}{{text[0]|lower}}{{text[1:]}}{% endmacro -%}

{%- for proc in procs %}
class {{proc.name}} : public DBBind<{{proc.params|length}},{{proc.columns|length}}>
{
public:
	{{proc.name}}(DBConnection& conn) : DBBind(conn, L"{CALL dbo.sp{{ proc.name }}{{ proc.questions }}}") { }

{%- for param in proc.params %}
  {%- if param.type == 'nvarchar' %}
	template<int32 N> void In_{{param.name}}(WCHAR(&v)[N]) { BindParam({{loop.index - 1}}, v); };
	template<int32 N> void In_{{param.name}}(const WCHAR(&v)[N]) { BindParam({{loop.index - 1}}, v); };
	void In_{{param.name}}(WCHAR* v, int32 count) { BindParam({{loop.index - 1}}, v, count); };
	void In_{{param.name}}(const WCHAR* v, int32 count) { BindParam({{loop.index - 1}}, v, count); };
  {%- elif param.type == 'varbinary' %}
	template<typename T, int32 N> void In_{{param.name}}(T(&v)[N]) { BindParam({{loop.index - 1}}, v); };
	template<typename T> void In_{{param.name}}(T* v, int32 count) { BindParam({{loop.index - 1}}, v, count); };
  {%- else %}
	void In_{{param.name}}({{param.type}}& v) { BindParam({{loop.index - 1}}, v); };
	void In_{{param.name}}({{param.type}}&& v) { _{{lower_first(param.name)}} = std::move(v); BindParam({{loop.index - 1}}, _{{lower_first(param.name)}}); };
  {%- endif %}
{%- endfor %}

{%- for column in proc.columns %}
  {%- if column.type == 'nvarchar' %}
	template<int32 N> void Out_{{column.name}}(OUT WCHAR(&v)[N]) { BindCol({{loop.index - 1}}, v); };
  {%- elif column.type == 'varbinary' %}
	template<typename T, int32 N> void Out_{{column.name}}(OUT T(&v)[N]) { BindCol({{loop.index - 1}}, v); }
  {%- else %}
	void Out_{{column.name}}(OUT {{column.type}}& v) { BindCol({{loop.index - 1}}, v); };
  {%- endif %}
{%- endfor %}

private:
{%- for param in proc.params %}
  {%- if param.type == 'int32' or param.type == 'TIMESTAMP_STRUCT' %}
	{{param.type}} _{{lower_first(param.name)}} = {};
  {%- endif %}
{%- endfor %}
};
{% endfor %}
  • 각 Procedure 마다 클래스를 작성한다.
  • Params 및 Columns의 개수도 알아서 맞춰준다.
  • 파라미터의 타입에 따라 함수를 다르게 생성하도록 한다.
     - 공통적으로 오른값을 다 받을 수 있다.
  • private 영역엔 사용될 변수를 둔다.

 

코드의 양이 많으므로 강의 중 이런 코드들에 대한 상세한 분석이 없었으므로,

완성본을 보면서 코드의 동작을 유추해 나가는 것이 꽤 도움이 됐다.

 

3. Generator 작성

이 부분은 사실상 기존에 작성된 PacketGenerator의 코드를 그대로 갖다가 쓸 수 있다.

 

import argparse
import jinja2
import XmlDBParser

def main():
    arg_parser = argparse.ArgumentParser(description = 'StoredProcedure Generator')
    arg_parser.add_argument('--path', type=str, default='H:/C++/Learning/Server/37_Procedure+Generator/GameServer/GameServer/GameDB.xml', help='Xml Path')
    arg_parser.add_argument('--output', type=str, default='GenProcedures.h', help='Output File')
    args = arg_parser.parse_args()

    if args.path == None or args.output == None:
        print('[Error] --path --output required')
        return

    parser = XmlDBParser.XmlDBParser()
    parser.parse_xml(args.path)

    file_loader = jinja2.FileSystemLoader('Templates')
    env = jinja2.Environment(loader=file_loader)
    template = env.get_template('GenProcedures.h')

    output = template.render(procs=parser.procedures)
    f = open(args.output, 'w+')
    f.write(output)
    f.close()

    print(output)

if __name__ == '__main__':
    main()

패킷 관련 내용이 제거되고 XML을 파싱 하는 내용이 들어가 있다 정도의 차이만을 가진다.

 

 

pushd %~dp0

GenProcs.exe --path=../../GameServer/GameDB.xml --output=GenProcedures.h

IF ERRORLEVEL 1 PAUSE

XCOPY /Y GenProcedures.h "../../GameServer"

DEL /Q /F *.h

PAUSE

배치 파일로 그렇게 다르진 않다.

옵션과 경로만 다시 잡아주면 끝.

 

마지막으로 스크립트를 pyinstaller를 사용해 실행 가능한 파일로 만들면 끝.

 

4. 스크립트 실행 및 테스트

이렇게 완성된 스크립트를 실행하면 아래와 같은 결과가 나온다.

 

#pragma once
#include "Types.h"
#include <windows.h>
#include "DBBind.h"

namespace SP
{
    class InsertGold : public DBBind<3,0>
    {
    public:
    	InsertGold(DBConnection& conn) : DBBind(conn, L"{CALL dbo.spInsertGold(?,?,?)}") { }
    	void In_Gold(int32& v) { BindParam(0, v); };
    	void In_Gold(int32&& v) { _gold = std::move(v); BindParam(0, _gold); };
    	template<int32 N> void In_Name(WCHAR(&v)[N]) { BindParam(1, v); };
    	template<int32 N> void In_Name(const WCHAR(&v)[N]) { BindParam(1, v); };
    	void In_Name(WCHAR* v, int32 count) { BindParam(1, v, count); };
    	void In_Name(const WCHAR* v, int32 count) { BindParam(1, v, count); };
    	void In_CreateDate(TIMESTAMP_STRUCT& v) { BindParam(2, v); };
    	void In_CreateDate(TIMESTAMP_STRUCT&& v) { _createDate = std::move(v); BindParam(2, _createDate); };

    private:
    	int32 _gold = {};
    	TIMESTAMP_STRUCT _createDate = {};
    };

    class GetGold : public DBBind<1,4>
    {
    public:
    	GetGold(DBConnection& conn) : DBBind(conn, L"{CALL dbo.spGetGold(?)}") { }
    	void In_Gold(int32& v) { BindParam(0, v); };
    	void In_Gold(int32&& v) { _gold = std::move(v); BindParam(0, _gold); };
    	void Out_Id(OUT int32& v) { BindCol(0, v); };
    	void Out_Gold(OUT int32& v) { BindCol(1, v); };
    	template<int32 N> void Out_Name(OUT WCHAR(&v)[N]) { BindCol(2, v); };
    	void Out_CreateDate(OUT TIMESTAMP_STRUCT& v) { BindCol(3, v); };

    private:
    	int32 _gold = {};
    };
};

 

잘 되나 확인하기 위해 다시 서버로 돌아가자.

 

 

{
	WCHAR name[] = L"Todd Howard";

	SP::InsertGold insertGold(*dbConn);
	insertGold.In_Gold(100);
	insertGold.In_Name(name);
	insertGold.In_CreateDate(TIMESTAMP_STRUCT{ 2023, 9, 1 });
	insertGold.Execute();
}

{
	SP::GetGold getGold(*dbConn);
	getGold.In_Gold(100);

	int32 id = 0;
	int32 gold = 0;
	WCHAR name[100];
	TIMESTAMP_STRUCT date;

	getGold.Out_Id(OUT id);
	getGold.Out_Gold(OUT gold);
	getGold.Out_Name(OUT name);
	getGold.Out_CreateDate(OUT date);

	getGold.Execute();

	while (getGold.Fetch())
	{
		GConsoleLogger->WriteStdOut(Color::BLUE,
			L"ID[%d] Gold[%d] Name[%s]\n", id, gold, name);
	}
}

정말 코드가 간결 해졌다는 것이 느껴진다.

실행 결과는 아래와 같다.

 

문제는 한글이 출력되지 않는다.

이게 참 이상한 게...

 

메모리를 봤을 때 깨지는 것 없이 잘 왔다는 것은 출력에 문제가 있다는 것을 의미한다.

하지만 작성한 로그 클래스는 아래와 같이 출력을 처리한다.

va_list ap;
va_start(ap, format);
::vwprintf(format, ap);
va_end(ap);

vwprintf()를 사용하기 때문에 와이드 캐릭터 출력에 이상이 없어야 한다.

이 상황을 이해할 수가 없어서 ChatGPT한테도 물어봤지만 영 소득이 없었다...

 

당장 이거만 판다고 해답이 나올 거 같지는 않아서 다음에 알아보는 게 좋을 것 같다.

 

 

 


 

 

여하튼 강의 자체는 마무리가 됐다.

게임 서버에 관해 이렇게 공부할 기회를 준 Rookiss 님에게 참으로 감사할 따름이다.

물론 모든 것을 이해했다고 볼 수는 없기 때문에 지속적인 복습이 필요할 것이다.

특히 모호하다 싶은 부분이 몇 가지 있으므로 이를 복습해 나갈 것 같다.

일부 사설 서버의 소스코드도 조금이나마 보이게 된 것이 그렇게 공부를 헛 하지는 않았나 보다.

안 보이던 코드가 보이게 되니 성취감을 느끼는 것 같아 동기부여에 매우 긍정적인 것 같아 만족스럽다.

'Study > C++ & C#' 카테고리의 다른 글

[C#] ###Clicker  (0) 2024.02.29
[C#] 자동화 플러그인 수정  (0) 2023.10.17
[C++ / DB] ORM  (0) 2023.09.01
[C++ / DB] XML Parser  (0) 2023.08.31
[C++ / DB] DBBind  (0) 2023.08.29
'Study/C++ & C#' 카테고리의 다른 글
  • [C#] ###Clicker
  • [C#] 자동화 플러그인 수정
  • [C++ / DB] ORM
  • [C++ / DB] XML Parser
BVM
BVM
  • BVM
    E:\
    BVM
  • 전체
    오늘
    어제
    • 분류 전체보기 (173)
      • Thoughts (14)
      • Study (75)
        • Japanese (3)
        • C++ & C# (50)
        • Javascript (3)
        • Python (14)
        • Others (5)
      • Play (1)
        • Battlefield (1)
      • Others (10)
      • Camp (73)
        • T.I.L. (57)
        • Temp (1)
        • Standard (10)
        • Challenge (3)
        • Project (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 본 블로그 개설의 목적
  • 인기 글

  • 태그

    포인터
    C++
    네트워크
    boost
    db
    Selenium
    c#
    프로그래머스
    스타필드
    클라우드
    Dalamud
    cloudtype
    네트워크 프로그래밍
    JS
    Asio
    OSI
    서버
    Python
    7계층
    IOCP
    bot
    docker
    FF14
    discord py
    로깅
    암호화
    베데스다
    Network
    discord
    Server
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
BVM
[C++ / Python / DB] ProcedureGenerator
상단으로

티스토리툴바