程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> COM技術初探(2)

COM技術初探(2)

編輯:關於VC++

三、純手工創建一個COM組件

1、從建工程到實現注冊

在這一過程中我們將完成三個步驟:創建dll的入口函數,定義接口文件,實現注冊功能

1.1創建一個類型為win32 dll工程

創建一個名為MathCOM的win32 dll工程。

在向導的第二步選擇"A smiple dll project"選項。當然如果你選擇一個空的工程,那你自己完成DllMain定義吧。

1.2 定義接口文件

生成一個名為MathCOM.idl的接口文件。並將此文件加入到剛才創建的那個工程裡。

//MathCOM.idl文件
// MathCOM.idl : IDL source for MathCOM.dll
//
// This file will be processed by the MIDL tool to
// produce the type library (MathCOM.tlb) and marshalling code.
import "oaidl.idl";
import "ocidl.idl";
  [
    uuid(FAEAE6B7-67BE-42a4-A318-3256781E945A),
    helpstring("ISimpleMath Interface"),
    object,
    pointer_default(unique)
  ]
  interface ISimpleMath : IUnknown
  {
    HRESULT Add([in]int nOp1,[in]int nOp2,[out,retval]int * pret);
    HRESULT Subtract([in]int nOp1,[in]int nOp2,[out,retval]int * pret);
    HRESULT Multiply([in]int nOp1,[in]int nOp2,[out,retval] int * pret);
    HRESULT Divide([in]int nOp1,[in]int nOp2,[out,retval]int * pret);
  };
  [
    uuid(01147C39-9DA0-4f7f-B525-D129745AAD1E),
    helpstring("IAdvancedMath Interface"),
    object,
    pointer_default(unique)
  ]
  interface IAdvancedMath : IUnknown
  {
    HRESULT Factorial([in]int nOp1,[out,retval]int * pret);
    HRESULT Fabonacci([in]int nOp1,[out,retval]int * pret);
  };
[
  uuid(CA3B37EA-E44A-49b8-9729-6E9222CAE844),
  version(1.0),
  helpstring("MATHCOM 1.0 Type Library")
]
library MATHCOMLib
{
  importlib("stdole32.tlb");
  importlib("stdole2.tlb");
  [
    uuid(3BCFE27E-C88D-453C-8C94-F5F7B97E7841),
    helpstring("MATHCOM Class")
  ]
  coclass MATHCOM
  {
    [default] interface ISimpleMath;
    interface IAdvancedMath;
  };
};

在編譯此工程之前請檢查Project/Setting/MIDL中的設置。正確設置如下圖:

圖1.4 midl的正確設置

在正確設置後,如編譯無錯誤,那麼將在工程的目錄下產生四個

文件名 作用 MathCOM.h 接口的頭文件,如果想聲明或定義接口時使用此文件 MathCOM_i.c 定義了接口和類對象以及庫,只有在要使用到有關與GUID有關的東西時才引入此文件,此文件在整個工程中只能引入一次,否則會有重復定義的錯誤 MathCOM_p.c 用於存根與代理 dlldata.c 不明

1.3 增加注冊功能

作為COM必須要注冊與注銷的功能。

1.3.1 增加一個MathCOM.def文件

DEF文件是模塊定義文件(Module Definition File)。它允許引出符號被化名為不同的引入符號。

//MathCOM.def文件
; MathCOM.def : Declares the module parameters.
LIBRARY   "MathCOM.DLL"
EXPORTS
  DllCanUnloadNow   @1 PRIVATE
  DllGetClassObject  @2 PRIVATE
  DllRegisterServer  @3 PRIVATE
  DllUnregisterServer  @4 PRIVATE

DllUnregisterServer 這是函數名稱 @4<――這是函數序號 PRIVATE

接下來大致介紹一下DllRegisterServer()和DllUnregisterServer()。(其他兩個函數的作用將在後面介紹)

1.3.2 DllRegisterServer()和DllUnregisterServer()

DllRegisterServer() 函數的作用是將COM服務器注冊到本機上。

DllUnregisterServer() 函數的作用是將COM服務器從本機注銷。

1.4 MathCOM.cpp文件

現在請將 MathCOM.cpp 文件修改成如下:

// MATHCOM.cpp : Defines the entry point for the DLL application.
//
#include "stdafx.h"
#include <objbase.h>
#include <initguid.h>
#include "MathCOM.h"
//standard self-registration table
const char * g_RegTable[][3]={
	{"CLSID\\{3BCFE27E-C88D-453C-8C94-F5F7B97E7841}",0,"MathCOM"},

	{"CLSID\\{3BCFE27E-C88D-453C-8C94-F5F7B97E7841}\\InprocServer32",
	                                                 0,
	                                                 (const char * )-1 /*表示文件名的值*/},
	{"CLSID\\{3BCFE27E-C88D-453C-8C94-F5F7B97E7841}\\ProgID",0,"tulip.MathCOM.1"},
	{"tulip.MathCOM.1",0,"MathCOM"},
	{"tulip.MathCOM.1\\CLSID",0,"{3BCFE27E-C88D-453C-8C94-F5F7B97E7841}"},
};
HINSTANCE  g_hinstDll;
BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 )
{
	g_hinstDll=(HINSTANCE)hModule;
    return TRUE;
}
/*********************************************************************
 * Function Declare : DllUnregisterServer
 * Explain : self-unregistration routine
 * Parameters : 
 * void -- 
 * Return : 
 * STDAPI  -- 
 * Author : tulip 
 * Time : 2003-10-29 19:07:42 
*********************************************************************/
STDAPI DllUnregisterServer(void)
{
	HRESULT hr=S_OK;
	char szFileName [MAX_PATH];
	::GetModuleFileName(g_hinstDll,szFileName,MAX_PATH);
	int nEntries=sizeof(g_RegTable)/sizeof(*g_RegTable);
	for(int i =0;SUCCEEDED(hr)&&i<nEntries;i++)
	{
		const char * pszKeyName=g_RegTable[i][0];
		long err=::RegDeleteKey(HKEY_CLASSES_ROOT,pszKeyName);
		if(err!=ERROR_SUCCESS)
			hr=S_FALSE;
	}
	return hr;
}
/*********************************************************************
 * Function Declare : DllRegisterServer
 * Explain : self Registration routine
 * Parameters : 
 * void -- 
 * Return : 
 * STDAPI  -- 
 * Author : tulip 
 * Time : 2003-10-29 19:43:51 
*********************************************************************/
STDAPI DllRegisterServer(void)
{
	HRESULT hr=S_OK;
	char szFileName [MAX_PATH];
	::GetModuleFileName(g_hinstDll,szFileName,MAX_PATH);
	int nEntries=sizeof(g_RegTable)/sizeof(*g_RegTable);
	for(int i =0;SUCCEEDED(hr)&&i<nEntries;i++)
	{
		const char * pszKeyName=g_RegTable[i][0];
		const char * pszValueName=g_RegTable[i][1];
		const char * pszValue=g_RegTable[i][2];
		if(pszValue==(const char *)-1)
		{
			pszValue=szFileName;
		}
		HKEY hkey;
		long err=::RegCreateKey(HKEY_CLASSES_ROOT,pszKeyName,&hkey);
		if(err==ERROR_SUCCESS)
		{
			err=::RegSetValueEx( hkey,
					pszValueName,
					0,
					REG_SZ,
					( const BYTE*)pszValue,
					( strlen(pszValue)+1 ) );
			::RegCloseKey(hkey);
		}
		if(err!=ERROR_SUCCESS)
		{
			::DllUnregisterServer();
			hr=E_FAIL;
		}
	}
   return hr;
}
STDAPI DllGetClassObject(REFCLSID rclsid ,REFIID riid,void **ppv)
{
	return CLASS_E_CLASSNOTAVAILABLE;
}
STDAPI DllCanUnloadNow(void)
{
	return E_FAIL;
}

我只是在此文件中加幾個必要的頭文件和幾個全局變量。並實現了 DllRegisterServer()和DllUnregisterServer()。而對於其他兩引出函數我只返回一個錯誤值罷了。

1.5 小結

現在我們的工程中應該有如下文件:

文件名                 作用
Stdafx.h和stdafx.cpp  預編譯文件
MathCOM.cpp           Dll入口函數及其他重要函數定義的地方
MathCOM.def           模塊定義文件
MathCOM.idl           接口定義文件(在1.2後如果編譯的話應該還有四個文件)

好了到現在,我的所謂COM已經實現注冊與注銷功能。

如果在命令行或"運行"菜單下項執行如下"regsvr32 絕對路徑+MathCOM.dll"就注冊此COM組件。在執行完此命令後,請查看注冊表項的HKEY_CLASSES_ROOT\CLSID項看看3BCFE27E-C88D-453C-8C94-F5F7B97E7841這一項是否存在(上帝保佑存在)。

如同上方法再執行一下"regsvr32 -u 絕對路徑+MathCOM.dll",再看看注冊表。

其實剛才生成的dll根本不是COM組件,哈哈!!!因為他沒有實現DllGetClassObject()也沒有實現ISmipleMath和IAdvancedMath兩個接口中任何一個。

讓我們繼續前行吧!!!

2、實現ISmipleMath,IAdvancedMath接口和DllGetClassObject()

2.1 實現ISmipleMath和IAdvancedMath接口

讓我們將原來的 CMath 類修改來實現ISmipleMath接口和IAdvancedMath接口。

修改的地方如下:

1) Math.h文件

/*@**#---2003-10-29 21:33:44 (tulip)---#**@
#include "interface.h"*/
#include "MathCOM.h"//新增加的,以替換上面的東東
class CMath : public ISimpleMath,
			  public IAdvancedMath
{
private:
	ULONG m_cRef;
private:
	int calcFactorial(int nOp);
	int calcFabonacci(int nOp);
public:
	CMath();
	//IUnknown Method
	STDMETHOD(QueryInterface)(REFIID riid, void **ppv);
	STDMETHOD_(ULONG, AddRef)();
	STDMETHOD_(ULONG, Release)();
	//	ISimpleMath Method
	STDMETHOD (Add)(int nOp1, int nOp2,int * pret);
	STDMETHOD (Subtract)(int nOp1, int nOp2,int *pret);
	STDMETHOD (Multiply)(int nOp1, int nOp2,int *pret);
	STDMETHOD (Divide)(int nOp1, int nOp2,int * pret);
	//	IAdvancedMath Method
	STDMETHOD (Factorial)(int nOp,int *pret);
	STDMETHOD (Fabonacci)(int nOp,int *pret);
};

2) Math.cpp文件

/*@**#---2003-10-29 21:32:35 (tulip)---#**@
#include "interface.h"  */
#include "math.h"
STDMETHODIMP CMath::QueryInterface(REFIID riid, void **ppv)
{//	這裡這是實現dynamic_cast的功能,但由於dynamic_cast與編譯器相關。
	if(riid == IID_ISimpleMath)
		*ppv = static_cast<ISimpleMath *>(this);
	else if(riid == IID_IAdvancedMath)
		*ppv = static_cast<IAdvancedMath *>(this);
	else if(riid == IID_IUnknown)
		*ppv = static_cast<ISimpleMath *>(this);
	else {
		*ppv = 0;
		return E_NOINTERFACE;
	}
	reinterpret_cast<IUnknown *>(*ppv)->AddRef();	//這裡要這樣是因為引用計數是針對組件的
	return S_OK;
}
STDMETHODIMP_(ULONG) CMath::AddRef()
{
	return ++m_cRef;
}
STDMETHODIMP_(ULONG) CMath::Release()
{
	ULONG res = --m_cRef;	// 使用臨時變量把修改後的引用計數值緩存起來
	if(res == 0)		// 因為在對象已經銷毀後再引用這個對象的數據將是非法的
		delete this;
	return res;
}
STDMETHODIMP CMath::Add(int nOp1, int nOp2,int * pret)
{
	 *pret=nOp1+nOp2;
	 return S_OK;
}
STDMETHODIMP CMath::Subtract(int nOp1, int nOp2,int * pret)
{
	*pret= nOp1 - nOp2;
	return S_OK;
}
STDMETHODIMP CMath::Multiply(int nOp1, int nOp2,int * pret)
{
	*pret=nOp1 * nOp2;
	return S_OK;
}
STDMETHODIMP CMath::Divide(int nOp1, int nOp2,int * pret)
{
	*pret= nOp1 / nOp2;
	return S_OK;
}
int CMath::calcFactorial(int nOp)
{
	if(nOp <= 1)
		return 1;
	return nOp * calcFactorial(nOp - 1);
}
STDMETHODIMP CMath::Factorial(int nOp,int * pret)
{
	*pret=calcFactorial(nOp);
	return S_OK;
}
int CMath::calcFabonacci(int nOp)
{
	if(nOp <= 1)
		return 1;
	return calcFabonacci(nOp - 1) + calcFabonacci(nOp - 2);
}
STDMETHODIMP CMath::Fabonacci(int nOp,int * pret)
{
	*pret=calcFabonacci(nOp);
	return S_OK;
}
CMath::CMath()
{
	m_cRef=0;
}

2.2 COM組件調入大致過程

1) COM庫初始化 使用CoInitialize序列函數(客戶端)

2)激活COM(客戶端)

3) 通過注冊表項將對應的dll調入COM庫中(COM庫)

4) 調用COM組件內的DllGetClassObject()函數(COM組件)

5)通過類廠返回接口指針(COM庫)這一步不是必需的

2.3 DllGetClassObject()實現

在MathCOM.cpp裡加入下列語句,

#include "math.h"

#include "MathCOM_i.c"

並將MathCOM.cpp裡的DllGetClassObject()修改成如下:

/*********************************************************************
 * Function Declare : DllGetClassObject
 * Explain : 
 * Parameters : 
 * REFCLSID rclsid  -- 
 * REFIID riid -- 
 * void **ppv -- 
 * Return : 
 * STDAPI  -- 
 * Author : tulip 
 * Time : 2003-10-29 22:03:53 
*********************************************************************/
STDAPI DllGetClassObject(REFCLSID rclsid ,REFIID riid,void **ppv)
{
	static CMath *pm_math=new CMath;
	if(rclsid==CLSID_MATHCOM)
		return pm_math->QueryInterface(riid,ppv);
	return CLASS_E_CLASSNOTAVAILABLE;
}

2.4 客戶端

接下來我們寫個客戶端程序對此COM進行測試。

新建一個空的名為 TestMathCOM 的 win32 Console 工程,將它添加到 MathCOM workspace 中。

在 TestMathCOM 工程裡添加一個名為 main.cpp 的文件,此文件的內容如下:

//main.cpp文件
#include <windows.h>
#include "../MathCOM.h"//這裡請注意路徑
#include "../MathCOM_i.c"//這裡請注意路徑
#include <iostream>
using namespace std;
void main(void)
{
	//初始化COM庫
	HRESULT hr=::CoInitialize(0);
	ISimpleMath * pSimpleMath=NULL;
	IAdvancedMath * pAdvancedMath=NULL;
	int nReturnValue=0;
	hr=::CoGetClassObject(CLSID_MATHCOM,
 			CLSCTX_INPROC,
			NULL,IID_ISimpleMath,
			(void **)&pSimpleMath);
	if(SUCCEEDED(hr))
	{
		hr=pSimpleMath->Add(10,4,&nReturnValue);
		if(SUCCEEDED(hr))
			cout << "10 + 4 = " <<nReturnValue<< endl;
		nReturnValue=0;
	}
	// 查詢對象實現的接口IAdvancedMath
	hr=pSimpleMath->QueryInterface(IID_IAdvancedMath, (void **)&pAdvancedMath);	
	if(SUCCEEDED(hr))
	{
		hr=pAdvancedMath->Fabonacci(10,&nReturnValue);
		if(SUCCEEDED(hr))
			cout << "10 Fabonacci is " << nReturnValue << endl;	
	}
	pAdvancedMath->Release();
	pSimpleMath->Release();
	::CoUninitialize();
	::system("pause");
	return ;
}

關於如何調試dll請參閱附錄A

2.5 小結

到現在我們應該有 2 個工程和 8 個文件,具體如下:

工程  文件 作用 MathCOM Stdafx.h 和 stdafx.cpp 預編譯文件   MathCOM.cpp Dll入口函數及其他重要函數定義的地方   MathCOM.def 模塊定義文件   MathCOM.idl 接口定義文件(在1.2後如果編譯的話應該還有四個文件)   math.h和math.cpp ISmipleMath,IadvancedMath接口的實現類 TestMathCOM Main.cpp MathCOM的客戶端,用於測試MathCOM組件

在此部分中我們已經完成一個可以實用的接近於完整的 COM組件。我們完成了此COM組件的客戶端。如果你已經創建COM實例的話,你可能會發現在此部分的客戶端並不是用CoCreateInstance()來創建COM實例,那是因為我們還沒有在此COM組件裡實現IClassFactory接口(此接口在下一部分實現)。

通過這個例子,我希望大家明白以下幾點:

1) DllGetClassObject()的作用,請參看COM組件調入大致過程這一節,同時也請將斷點打在DllGetClassObject()函數上,仔細看看他的實現(在沒有實現IClassFactory接口的情況下)和他的傳入參數。

2) 為什麼在這個客戶端程序裡不使用CoCreateInstance()來創建COM實例而使用CoGetClassObject()來創建COM實例。你可以試著用CoCreateInstance()來創建Cmath,看看DllGetClassObject()的第一參數是什麼?

3) 實現IClassFactory接口不是必需的,但應該說是必要的(如何實現請看下一章)

4) 應掌握DllRegisterServer()和DllUnregisterServer()的實現。

5) 客戶端在調用COM組件時需要那幾個文件(只要由idl文件產生的兩個文件)

3、類廠

附錄

A 我對 dll 的一點認識

目標:寫幾個比較簡單的dll並了解**.dll與**.lib的關系。

一:沒有lib的dll

1.1建一個沒有lib的dll

1) 新建一個com_1.cpp文件(注意此dll根本沒有什麼用)

2) 在com_1.cpp寫下下面的代碼

3) 按下F5運行,所有的東西都按確定。

4) 應該出現如下錯誤:

Linking...
Creating library Debug/COM_1.lib and object Debug/COM_1.exp
LIBCD.lib(crt0.obj) : error LNK2001: unresolved external symbol _main
Debug/COM_1.exe : fatal error LNK1120: 1 unresolved externals

5)進入 project|setting,在 "C/C++" 屬性框的 "project Options" 裡把

"/D ''_console''" 修改成"/D ''_WINDOWS''"。

6)進入project|setting,在 "link" 屬性框的 "project Options" 裡增加下
面的編譯開關 "/dll "

增加的編譯開關大致如下:

kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib
ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /incremental:yes
/pdb:"Debug/COM_1.pdb" /debug /machine:I386 /out:"Debug/COM_1.dll" /implib:"Debug/COM_1.lib"
/pdbtype:sept

注意:"/dll"應該與後面的開關之間有一個空格

//com_1.cpp
#include <objbase.h>
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved) 
{
		HANDLE g_hModule;
	switch(dwReason)
	{
	case DLL_PROCESS_ATTACH:
		g_hModule = (HINSTANCE)hModule;
		break;
	case DLL_PROCESS_DETACH:
		 g_hModule=NULL;
		 break;
	}
}

現在可以編譯了,這小片段代碼將會生成一個dll,但這個dll是沒有用的。沒有引出函數和變量。

1.2 調試沒有 lib 的 dll

1) 新建一個工程 Client,工程類型為 console,將上面創建的 dll copy 到 client 工程目錄下

2) 增加 Client.cpp(代碼見下)到工程 Client 中去

3) 選中 Client 工程,並在 project|setting|debug|Category 下拉框,如圖:

圖1.4 調試

注意這是一種調試 dll 的方法

5) 現在可以在Client和COM_1.dll裡打斷點調試了。

在這裡我們只能調試DllMain()函數,因為那個dll裡除了就沒別的東西了,下面我開始 增加一點東西。

二:帶有lib的dll

2.1 創建一個帶有lib的dll

我們在原來的基礎上讓上面的代碼產生一個lib了。新的代碼如下:

#include <objbase.h>
extern "C" __declspec(dllexport)  void tulip (void)
{
	::MessageBox(NULL,"ok","I''am fine",MB_OK);
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved) 
{
	HANDLE g_hModule;
	switch(dwReason)
	{
	case DLL_PROCESS_ATTACH:
		g_hModule = (HINSTANCE)hModule;
		break;
	case DLL_PROCESS_DETACH:
		 g_hModule=NULL;
		 break;
	}
	return TRUE;
}

在這個dll裡,我們引出一個tulip函數。如果此時我們想要在客戶調用此函數應該用什麼方法呢?

上面的代碼除了生成dll外,他比第一個程序多產生一個lib文件,現在應該知道dll與lib的關系吧。Lib文件是dll輸出符號文件。如果一個dll沒有任何東西輸出那麼不會有對應的lib文件,但只要一個dll輸出一個變量或函數就會相應的lib文件。總的說來,dll與lib是相互配套的。

當某個dll他有輸出函數(或變量)而沒有lib文件時,我們應該怎麼調用 dll 的函數呢?請看下面的方法。

2.2 調試帶有引用但沒有頭文件的 dll

注意:本方法根本沒有用 COM_1.lib 文件,你可以把 COM_1.lib 文件刪除而不影響。

此時的客戶端代碼如果下:

#include <windows.h>
int main(void)
{
	//定義一個函數指針
	typedef void (  * TULIPFUNC )(void);
	//定義一個函數指針變量
	TULIPFUNC tulipFunc;
	//加載我們的dll
	HINSTANCE hinst=::LoadLibrary("COM_1.dll");
	//找到dll的tulip函數
	tulipFunc=(TULIPFUNC)GetProcAddress(hinst,"tulip");
	//調用dll裡的函數
	tulipFunc();
	return 0;
}

對於調用系統函數用上面的方法非常方便,因為對於User32.dll,GUI32.dll這種dll,我沒有對應的lib,所以一般用上面的方法。

三:帶有頭文件的dll

3.1 創建一個帶有引出信息頭文件的dll

如果用上面的方法調用我們自己創建的dll那太煩了!因為我們的dll可能沒有像window這樣標准化的文檔。可能過了一段時間後,我們都會忘記dll內部函數的格式。再如當我們把此dll發布客戶時,那個客戶肯定會在背後罵你的!

這時我們需要一個能了解dll引出信息途徑。我創建一個.h文件。繼續我們旅途。

我們的dll代碼只需要修改一點點,代碼如下:

#include <objbase.h>
#include "header.h"//看到沒有,這就是我們增加的頭文件
extern "C" __declspec(dllexport)  void tulip (void)
{
	::MessageBox(NULL,"ok","I''am fine",MB_OK);
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved) 
{
	HANDLE g_hModule;
	switch(dwReason)
	{
	case DLL_PROCESS_ATTACH:
		g_hModule = (HINSTANCE)hModule;
		break;
	case DLL_PROCESS_DETACH:
		 g_hModule=NULL;
		 break;
	}
	return TRUE;
}

而 header.h文件只有一行代碼:

extern "C" __declspec(dllexport) void tulip (void);

3.2 調試帶有頭文件的dll

而此時我們的客戶程序應該變成如下樣子:(比第二要簡單多了)

#include <windows.h>
#include "..\header.h"//注意路徑
//注意路徑,加載 COM_1.lib 的另一種方法是 Project | setting | link 設置裡
#pragma comment(lib,"COM_1.lib")
int main(void)
{
	tulip();//只要這樣我們就可以調用dll裡的函數了
	return 0;
}

四:小結

今天講了三種 dll 形式,第一種是沒有什麼實用價值的,但能講清楚 dll 與 lib 的關系。我們遇到的情況大多數是第三種,dll 的提供一般會提供 **.lib 和 **.h 文件,而第二種方法適用於系統函數。

希望各位高手指正與交流,

注:今天一時興起,寫了上面的東西,本來我是總結一下有關 COM 的東西,但寫著寫著就成這個樣子,COM 也是從 dll 起步的。

(全文完)

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved