剎那搜尋工坊 葉健欣 2008.8.8
2008.8.12 修訂
2008.8.13 支援Linux , svn revision 22
2008.10.28 多媒體影音教學(需安裝 wink) 源碼解說 追縱
1. ficl_gtk_symbols.c
2. ficl_gtk_signal.c
3. ficl_gtk_builder.c
在閱讀本教材之前,如果你從來沒聽過Forth 這個程式語言,請先閱讀Wikipedia 上的解說。本教材使用到的Forth 指令很少,大概只有 VALUE, DROP, ' (tick) , ." , CR 等幾個。對Forth 稍有涉獵者應該都很熟悉。
由於筆者於本週開始正式接觸 GTK,在此之前,沒有看過其技術文件或源碼,當然更沒有開發過任何基於 GTK 的應用,所以本教材絕不是為 GTK 高手而選寫的。
當然,你也最好具備一些 GUI的基本觀念,如視覺元件、事件的觸發、訊息的迴路等。如果你還不太清楚也沒關係,只要能將本教材認真讀完,並追縱原始碼,將會對 GUI的工作原理有一個比較全面的認識。
本教學是講解 KsanaGTK 的工作原理。KsanaGTK 是一個 GTK 和 Forth 的Binding。
GTK 的計劃主要就是衝著 QT 而來,QT 鼓勵直接用C++開發,其訴求是write once , compile everywhere。而GTK一開始就不假設大家會用C來撰寫GTK應用,而是可以搭配其他的腳本語言,得到快速原型開發、高度彈性以及免除跨平台編譯的麻煩。
GTK 官方網站公佈了15 種程式語言的介面,有了這麼多的其他語言抬轎,對GTK來說,如虎添翼,快速拓展使用群;對非 C 的程式員來說,可以繼續使用自己熟悉的腳本語言,來使用GTK這樣一個高效能、高品質的GUI元件庫,是魚與熊掌兼得的好事。
但讓其他腳本語言調用GTK的功能之前,需要一個所謂的Language Binding的機制,來橋接兩個語言。不同的語言就像兩個世界,有各自的「習慣」,如函式呼叫的規範、堆疊、暫存器使用方式、記憶體的管理等等。Language Binding 橋接了兩個彼此不往來的世界,也就是兩個不同的「執行情境」(Execution Context),一般認為是比較複雜高深的技術。
PHP, Perl, Python 這幾個主流腳本語言的 GTK Binding 動輒要數MB ,即使是號稱最精簡的Lua-GTK 也要273KB,如果你沒有過人的意志,是不容易一窺全貌的。腳本語言的語法看起來都很簡單,但其實上內部做了大量的苦工,從軟體工程的觀點來看,只是將複雜度往內推。因此,即使高階程式有豐富的編程經驗,但對其內部結構所知還是有限,我覺得十個python/perl/php programmers 之中很難找到一個有能力讀python 的C source 之人,不知這樣說有沒有過份?
KsanaGTK 則展現了一個新的可能,由於 Forth 的執行機制本身就很精巧(請參考另一個教學:剎那極簡虛擬機。Forth 沒有任何語法上的糖衣,沒有任何被隱藏的複雜度。事實上,任何一個稱職的Forth 程序員,都應該有能力自己打造一套自己的Forth。KsanaGTK 目前不用 KsanaVM ,而採用 FICL (Forth inspired control language) ,主要是因為 Ficl 比較完整,文件也齊全。而且教學的重心在於橋接的介面,所以用那套Forth其實關係不大。
主流的腳本語言和 Forth,背後有著完全不一樣的哲學。主流的腳本語言就像西醫治病:醫生需要長期的訓練、醫療的設備、藥物的配方等等,背後都由極為龐大的產業和機構所支撐。醫生、病名、藥名越來越多,大家越來越搞不懂,搞不懂就會害怕,而這份恐懼讓大家願意繼續受醫療體系所宰制。而Forth 就像中醫,你只需對身體的系統觀的理解 (氣血、經脈、臟腑、時節) 和掌握之間的互動關係,中醫所謂的治療,只是幫助你的身體回到平衡的狀態,以啟動自我修復機制,所以沒有醫生、沒有藥、可以用針炙、用推掌刮痧,一樣有療效。到了最高境界,就不必借助任何外部工具,只要打坐、吐納、練氣即可怯百病、得高壽。這不是老子說的「為學日益、為道日損」的最佳實證嗎?
所以我們可以看到 KsanaGTK 只有3個檔 12KB,約300行C程式,就溝通了 GTK和 Forth 兩個世界。這個規模的程式碼,和大一新生的課堂作業相差無幾,相信每個人都有能力遍讀。當然KsanaGTK 的完整性也許無法和其他語言相提並論,不過,對於理解語言之間的Binding原理,這樣就足夠了。當你完全搞懂了 KsanaGTK 的工作原理,再去看其他的語言Binding ,就會覺得問題的本身很簡單,只是有人將它搞得很複雜。所以,不要再沉迷於各種新奇語言、語法的花拳繡腿,請回到計算機的本質,澈底理解之、熟練之、再內化為你思路的一部份,內力就是這樣練出來的。而 Forth 就是捷徑,金城老師稱之為「人機合一的無上心法」。
本教學的製作環境是在 Windows Vista 和 Visual C++ studio 2008, 先聲明一下個人對Windows 沒有偏愛、也不特別討厭,Visual C++ 是公司付的正版專業版。KsanaGTK 是用很平舖直叙的C所寫就,目前可以在Windows 和 Linux 底下運行。以下就來逐行講解實現 KsanaGTK 的三個模組。
這個模組的功能是連結GTK的函式,在Windows 下,需要先載入ligtk-win32-2.0-0.dll等 6個 DLL,放到一個 dl_handle的陣列之中,見 gtk_dll_init()。lookup_symbol的功能依名稱,到每個DLL尋找函式,在Windows 下,是由GetProcAddress實現,在Linux 下則為 dlsym。
本模組只實現了一個Forth 的字,稱為 gtk: ,用法是
1 gtk: gtk_window_new \ 連結名為 gtk_window_new 的函式, 此函式需要一個參數
然後,就可以這樣用
0 gtk_window_new \ 創建一個 window 視覺元件
所以,連結GTK 的方式就是先查手冊,確認函式名稱以及要傳入參數的個數,再用 gtk: 來建造。更多的例子請看gtk.f。可以將 gtk: 想像成一個「橋樑產生器」,每次使用,就會建造出一個溝通 forth 和 gtk 兩個世界的橋樑。
接下來我們看看這個橋樑產生器如何實現:
void ficlCompileGtkMakeSymbol(ficlVm *vm)
{
ficlDictionary *dict=ficlSystemGetDictionary(vm->callback.system);
// 創造 gtk: 這個新字,由 ficl_make_gtk_func 實現
ficlDictionarySetPrimitive(dict,"gtk:",ficl_make_gtk_func,FICL_WORD_DEFAULT);
}
void ficl_make_gtk_func(ficlVm *vm)
{
// 取得函式的參數個數 (gtk: 之前的數字)
int narg=ficlStackPopInteger(vm->dataStack);
// 取得函式的名稱 (gtk:之後的字串)
ficlString name = ficlVmGetWord(vm);
// forth 字典,建造橋樑的所在
ficlDictionary *dic=vm->dictionary;
// 指向 gtk api 的進入點
void *gtk_api;
// 函式名
char funcname[0x100];
// 將gtk: 後面的字串複製到 funcname
strncpy_s(funcname,sizeof(funcname),name.text,name.length);
// 尋找進入點
gtk_api=lookup_symbol(funcname);
if (gtk_api) // 非零,找到了
{
// 建造一個新的 Forth 字,這個字由 ficlCEntryPoint
ficlDictionaryAppendWord(dic, name, (ficlPrimitive)ficlCEntryPoint, FICL_WORD_DEFAULT );
} else {
// 找不到的話,就丟出錯誤訊息
ficlVmThrowError(vm, "gtk symbol %.*s not found", FICL_STRING_GET_LENGTH(name), FICL_STRING_GET_POINTER(name));
}
}
// 要存兩項資料,此API 的進入點
ficlDictionaryAppendUnsigned(dic,narg);
// 以及參數個數
ficlDictionaryAppendUnsigned(dic,(int)gtk_api);
因此,每搭一座橋的成本是一個Forth 字,加上8bytes 的空間,只比建造一個Forth 變數多 4 個 bytes 而已。
在上面的程式碼我們可以看到,gtk: 所建造的字,都將調用ficlCEntryPoint這個函式,也就是說,在 ficl 中,任何呼叫 GTK API 的字,都會先進入這個函式。ficlCEntryPoint 的主要任務就是將Forth 的堆疊搬到 C 的堆疊,然後呼GTK 函式。
static void ficlCEntryPoint(ficlVm *vm)
{
// 取得Forth 的param field指標,緊接在這個指標後面的 8個bytes ,就是使用 gtk: 編入字典的兩項數據
int *param=(int*)(vm->runningWord->param);
// 注意這和上面的粉紅色區是對應的
int p,i,r;
// 將 Forth 的堆疊資料搬到 C , 這裡要用到x86組合語言的push 指令,
// 因為 C 不支持在編譯時期未知參數個數的函式呼叫
for (i=0;i<narg;i++) {
p=ficlStackPopInteger(vm->dataStack);
__asm push p
}
__asm {
// 呼叫 GTK 函式,取得返回值,放入 r
call gtk_api
mov r, eax
// 回復 ESP 的數值
mov eax, narg
shl eax,2
add esp, eax
}
// 將 r 放到 Forth 堆疊,在這裡我們稍微偷懶一下,有些 GTK 的函式返回值宣告為 void ,但我們不管,一律放上堆疊,如果不需要,只要加一個DROP 指令即可
ficlStackPushInteger(vm->dataStack,r);
}
// 取得 GTK 函式進入點
void *gtk_api=(void*)(*param);
// 接著取得此函式的參數個數
int narg = *(param+1);
上面這段程式並不長,但背後涉及了一些比較少見的觀念:一般我們所謂的編寫程式,都是在編寫執行階段的行為,所謂的 runtime behavior programming,但ficl_make_gtk_func是在定義一種compile time behavior,或者說是 metaprogramming ,因為當執行 gtk: 時,會即時產生了新的「函式」出來。如果不用 Metaprogramming 的技術,那對每個GTK API ,我們都需要一個對應的C 函式,來處理參數的轉換,而由於GTK包含了成千上萬個函式,不用這樣的技巧,將大幅增加程式碼的大小,也讓維護變得極為麻煩。
到目前為止,我們只打造了單向的橋樑,gtk: 可以讓 forth 呼叫任何 gtk 的函式,但是在GUI programming 中,有一個非常關鍵,稱為 signal (有時也叫 event) 和 callback ( 有時也叫 handler ) 的機制。比方說,畫面上有一個按鈕,當使用者按下時,GUI 會觸發一個 clicked 的事件,而應該有一個對應的函式來負責執行一個動作。由於這些當特定事件發生時才會被執行的函式,是由 GUI 觸發執行的,所以稱為 callback 回呼函式。除了讓Forth 來調用 GTK ,我們也必須可以使用Forth 來編寫 callback ,讓 GTK 來調用,這樣才算完成雙向的橋樑。
在 GTK 之中,聯繫元件、事件和回呼函式的API 叫作 gtk_signal_connect,其宣告如下:
gint gtk_signal_connect (GtkObject *object, gchar *name, GtkSignalFunc func, gpointer func_data)
這裡object 是元件,name 是signal 的名稱,func 是回呼函式的指標,func_data 是用戶額外要的資料,它會事件發生生,由GTK一並傳入 callback 。 GtkSignalFunc 並沒有明確的宣告,它將視訊息的類型而有所不同,這是GTK 一個非常有意思的設計,相較於 wxWidget ,提供了更大的彈性。
舉例來說,處理"clicked"事件的回呼,具有以下的形式:
而 "delete_event" 事件的回呼,則是
on_button_click (GtkWidget *widget, gpointer data);
on_delete_event ( GtkWidget *widget, GdkEvent *event, gpointer data );
很顯然地,我們無法使用gtk: 來建造 gtk_signal_connect 介面,因為在 forth 的執行環境中,我們無法提供符合 C 呼叫規範的函式指標。因此,我們需要對gtk_signal_connect 做特別的處理,然後,在forth 中我們可以用以下的方式來聯繫事件和forth 回呼函式:
: on_click ( widget userdata -- ) 2drop ." hello!!" ; \ 一個簡單的forth word,只是印出 hello
button1 z" clicked" ' on_click 101 gtk_signal_connect \ 語意:元件button1發生事件 clicked 時執行 on_click
\ 101 使用者額外提供的資訊
讓我們來看看 gtk_signal_connect 的實現方式。
void ficlCompileGtkSignal(ficlVm *vm)
{
//注意,這個 vm->callback 和 GTK 無關,不要搞混。這行的目的只是要取得 ficl 的字典而已。
ficlDictionary *dict=ficlSystemGetDictionary(vm->callback.system);
//首先我們要改寫 gtk_signal_connect 的處理方式
ficlDictionarySetPrimitive(dict,"gtk_signal_connect",ficl_gtk_signal_connect,FICL_WORD_DEFAULT);
}
因為在runtime 時事件和回呼函式的綁定可能頻繁地發生,我們不希望每次呼叫 gtk_signal_connect 都在forth dictionary 配置空間。因此,需要動態配置一個結構,來記錄調用回呼函式所需的資料,這個結構所佔據的記憶體會在signal 和 callback 的聯繫解除時被釋放
typedef struct callback_context {
ficlVm *vm; // 虛擬機的指標
ficlWord *forthword; // 指向forth callback
void *widget; // 發出這個signal 的元件
void *userdata; // 綁定signal/callback 時,用戶的額外參數
int n_param; // 參數個數
int return_type ; // 返回的型別 (只有兩種,有返回值或無返回值 )
};
這是gtk_signal_conenct 的本體,主要的工作填寫callback_context 結構,將之當作 user_data 傳入 GTK,統一的入口是signal_callback,每當GTK執行回呼,都會先跳進這裡。
GTK 會在聯繫解除時自動呼叫這個函式
// gtk_signal_connect ( widget signal_name forth_word user-data -- result )
void ficl_gtk_signal_connect(ficlVm *vm)
{
int r,signal_id;
GSignalQuery signal_query; // 這裡會放 signal 的細節
struct callback_context *cb;
// 取出 forth 參數
gpointer func_data=ficlStackPopPointer(vm->dataStack);
void *forthword=ficlStackPopPointer(vm->dataStack);
char *name=ficlStackPopPointer(vm->dataStack);
GtkObject *object=(GtkObject *)ficlStackPopPointer(vm->dataStack);
// 為結構 callback_context 配置記憶體
cb=g_slice_new(struct callback_context);
//填寫 callback_context
cb->vm=vm;
cb->forthword=forthword;
cb->userdata=func_data; // 這是從 forth 提供的,因為我們呼叫 g_signal_connect_data 時要佔用,所以要在這裡保存
cb->widget=object;
// 取得 signal id
signal_id = g_signal_lookup(name, G_OBJECT_TYPE(object));
// 查詢 signal 的細節
g_signal_query(signal_id,&signal_query);
// 此signal callback 的參數個數
cb->n_param=signal_query.n_params ;
// 此signal 的返回型別
cb->return_type=signal_query.return_type ;
//這裡要用 G_CONNECT_SWAPPED,這樣GTK調用callback 時會用相反的順序傳入,讓signal_callback比較好處理
r=g_signal_connect_data(object,name,(GCallback)signal_callback,cb,_free_callback_context,G_CONNECT_SWAPPED);
ficlStackPushInteger(vm->dataStack,r);
}
void _free_callback_context(gpointer data, GClosure *closure)
{
g_slice_free(struct callback_context, (struct callback_context*) data);
}
所有的callback 都會先進入這裡,再跳進Forth 函式。注意這是一個未知參數個數的函式,就像 printf 一樣。我們使用C 的 va_arg函式來取得實際傳入的參數。
int signal_callback (gpointer context,...)
{
va_list ap;
struct callback_context *cb=context;
//從callback_context 中 取回執行所需的資料
int i;
void * param;
//每個callback 的第一個參數總是 widget
ficlStackPushPointer(cb->>vm-->>dataStack,cb->widget);
//將參數依序移到 Forth
va_start(ap, context);
for (i=0; i<cb->n_param; i++) {
param=va_arg(ap,void*); //取得本函式傳入的參數
ficlStackPushPointer(vm->dataStack,param); //推入 forth 堆疊
}
va_end(ap);
//最後一個參數就是用戶額外資料
ficlStackPushPointer(cb->>vm->dataStack,cb->userdata);
//呼叫 Forth 函式
ficlVmExecuteXT(vm, cb->forthword);
//看看callback 有沒有返回值。
if (cb->return_type!=G_TYPE_NONE) {
return ficlStackPopInteger(cb->>vm->dataStack);
} else return 0;
}
至此,從Forth 到 GTK ,以及從 GTK 到 Forth 的雙向橋樑已建造完成。我們就可以用 Forth 來開發基於 GTK 的應用,享受GTK這個開源社群傑出的成果。
雖然本質上所有的視窗元件都是使用API 來建生,但從軟體開發的角度而言,將使用者介面從程式碼分離出來,是主流的做法。 在 GTK 中有個 GtkBuilder的機制,其前身是 libglade ,其原理是將視覺元件的擺放位置、顯示訊息、功能表以及事件的綁定,存成XML 檔案,在執行期間再載入。這樣做的好處很多,首先,程式功能的開發和畫面的設計可以獨立進行而不相互干擾。其次,由於 XML 和語言無關,因此在系統的開發之初,可以使用腳本語言來做快速原型開發,等規格和介面穩定下來,再用C或C++來撰寫。第三,XML 本身是跨平台的文字檔,所以畫面自然可以跨平台,更新畫面也非常容易。通常系統的維護,畫面的調整佔了70%以上,用使用XML描述 UI,修改畫面不必動到程式,也就不必重新除錯和測試,長期下來,可以節省可觀的維護成本。
這裡需要用到三個Forth 的字,分別是 gtk_builder_new , g_object_unref 和gtk_builder_get_object,以下是使用範例。後二者直接用 gtk: 製造。
s" calc.ui" gtk_builder_new VALUE Builder \ 載入calc.ui 檔案
Builder s" CalcWindow" gtk_builder_get_object TO CalcWindow \ 取得 名為 CalcWindow 的元件
Builder s" CalcEntry" gtk_builder_get_object TO CalcEntry \ 取得 名為 CalcEntry 的元件
Builder g_object_unref \ 釋放 builder, 不過CalcWindow 和 CalcEntry 還是有效的
calc.ui 是一個 XML 檔,它是由 Glade-3 視覺化介面製作工具所產生(類似於VB, Delphi),再經由 gtk-builder-convert 而得,不建議直接用手動編修。
gtk_builder_new 載入介面 UI XML檔,並且將 XML所指定的 callbacks 和 forth 的字聯繫起來。也就是說,這個函式會自動地替每個在 XML 定義的元件,執行 gtk_signal_connect 的動作,程式碼可以大幅簡短,同時又增加了彈性。舉例來說,只要先寫好某個動作 FileSave ,UI 中可以使用功能表來呼叫 FileSave,或是用一個接鈕,都不必動到程式。
void ficl_gtk_builder_new(ficlVm *vm)
{
GtkBuilder *builder;
char *str=ficlStackPopPointer(vm->dataStack);
int r;
// 創建 GtkBuilder 元件
builder=gtk_builder_new();
//載入 XML UI 定義檔
r=gtk_builder_add_from_file(builder,str,NULL);
if (!r) { // 無法載入則顯示錯誤訊息
g_warning ("error loading GtkBuilder ui file %s", str);
g_object_unref(G_OBJECT(builder));
ficlStackPushPointer(vm->dataStack,0);
return;
}
// 將 UI 中定義的callback 和 forth 聯繫起來
// 對每個XML定義的callback,會調用 gtk_builder_connect_signal_forth 一次。
// 同時傳入 vm ,以便 gtk_builder_connect_signal_forth 要用到
gtk_builder_connect_signals_full (builder,gtk_builder_connect_signal_forth,vm);
//留下 builder 的指標
ficlStackPushPointer(vm->dataStack,builder);
}
先到forth 字典查詢是否存在於指定的callback,如果找到,就呼叫 gtk_signal_connect。
static void gtk_builder_connect_signal_forth (GtkBuilder *builder,
GObject *object, const gchar *signal_name,
const gchar *callback_name, GObject *connect_object,
GConnectFlags flags, gpointer user_data)
{
ficlVm *vm= (ficlVm*)user_data;
ficlWord *forthword = NULL;
ficlString str;
//準備 forth 的字串
str.text=(gchar*)callback_name;
str.length=strlen(callback_name);
// 到字典查查看
forthword = ficlDictionaryLookup(ficlVmGetPrivateDictionary(vm), str);
if (!forthword) forthword = ficlDictionaryLookup(ficlVmGetDictionary(vm), str);
// 查無此字
if (!forthword) {
g_warning ("Could not find signal callback '%s'", callback_name);
return;
}
//準備呼叫 forth 字來達成聯繫的動作
ficlStackPushPointer(vm->dataStack,object);
ficlStackPushPointer(vm->dataStack,signal_name);
ficlStackPushPointer(vm->dataStack,forthword);
ficlStackPushPointer(vm->dataStack,user_data);
// 重用我們之前定義好的 gtk_signal_connect
ficl_gtk_signal_connect(vm);
//返回值就不要了
ficlStackPopPointer(vm->dataStack);
}
對於別人寫的程式,看十遍不如追縱一遍,因為看只是靜態,程式對你而言還是死的,而追縱才有辦法觀測到活生生的程式。打個比方說,想要了解生物體,最好的方式並不是解剖,生物一死,氣脈停止,還能觀察出什麼子丑寅卯來?當然是要在活著的情況下,才有辦法觀察到情志、臟腑、飲食之間的影響。同理,程式也是一樣,觀察程式運行的最佳工具,就是單步除錯器(Step Debugger),不要小看這個,一個人的編寫程式的能力,很大程度取決於對除錯器的掌握程度。
因此,大家如果對以上講解的實作,那怕還有一絲一毫的疑惑,都應該祭出除錯器,一步步執行,觀察變數、記憶體和暫存器的變化,再難的程式,只要多追幾步,必定會豁然開朗。只要將這個技能練熟,就算是數十萬行的 Linux Kernel (用user mode linux),也可以用同樣的技巧來庖丁解牛,所有的奧秘將無所遁形。
建議大家先在 ficl_make_gtk_func 設中斷點,觀察當執行 gtk: 時,新的介面如何被建置。然後在ficlCEntryPoint設中斷點,觀察Forth 的參數如何轉換為C 的參數,並入gtk 函式的過程。接下來觀看 gtk_signal_connect ,掌握signal 和callback聯繫的過程。最後在 sigal_callback 設中斷點,回到GUI程式按下按鈕,就會停在 sigal_callback ,仔細觀察從 GTK 進入到 Forth 的流程。
只要掌握了這四處重點,就大致理解了整個系統的運作。為驗證自己的理解是否澈底,可以試著將 FICL 換為其他的 Forth ,或是將 GTK 換為 wxWidget, MicroWindow, FLTK等 tool kits 。只要內力具足,學習各種花招都輕而易舉,而且再平淡無奇的招式都可以發揮威力。
2)Binding 分為兩部份,首先是讓 Forth 可以呼叫 GTK function ,這是透過 gtk: 來實現的。
3)gtk: 可以想像成一個只有兩個field和一個 method 的class,每次執行 gtk: 都會產生一個instance,以記錄某個gtk函式的進入點和參數個數。執行這個forth word ,就是執行這個instance 的method,也就是調用 GTK 的服務。
4)GUI 發生了任何動作稱為 signal ,對signal做出的反應由 callback 來完成。視覺元件(按鈕、功能表之類)、signal 和 callback 之間的聯繫,可以用程式碼,或是使用 glade 用圖形化的方式來指定。
5)當某個事件發生時,比方說某個按鈕被按下,此時,GTK 會產生一個 clicked 事件,並搜尋看看有沒有負責對這個事件回應的callback。如果沒有,就當什麼事都沒發生過。如果有,就呼叫這個 callback 。
6)為了可以用forth 來撰寫callback ,首先在聯繫signal 和callback 之際,配置一小塊記憶體callback_context來放虛擬機的handle等其他執行callback 所需的資料,同時我們規定所有的callback都先進入signal_callback。
7)當事件發生時,首先會進入 signal_callback ,在這裡我們依據callback_context,來處理參數的搬動,並進入 Forth word 。
8)從Forth 可以呼叫 GTK,也讓Forth 可以準備callback 供 GTK 呼叫,如此就完成了 Forth 和 GTK 兩個世界的溝通,使用者只要有能力查閱GTK 文件,就可以像其他語言如python/perl/lua 一樣,快速地開發GTK應用程式。
謝謝大家耐心看完本文,歡迎來信切磋和賜教。我的信箱是yapcheahshen@gmail.com