Monday, December 27, 2021

[用Golang寫小遊戲教學] 第四章 陣列(Array)與切片(Slice)

[前言]

陣列(Array)就好比是單排座位的列車,座位編號從0開始,共有N個座位且不可增加座位數量。
切片(Slice)很像陣列(Array),但它是可以增加座位數量。 Slice細節還有關於容量與長度的問題,我們就先忽略它~
所以,這兩種資料結構可以存放一長串的資料,這是在寫小遊戲中非常常使用到的功能。


[陣列(Array)]

陣列(Array):它的長度是固定的(fixed length),使用語法: " [數量] 型別" 定義。例如: [100]int

它比較沒有彈性,內部存放的資料都必須要有相同的型別(像是整數、字串等等)。

已經學習過變數宣告,我們舉兩個例子(變數宣告與變數短宣告)來宣告變數型別為陣列:

Saturday, December 25, 2021

[用Golang寫小遊戲教學] 第一章 Golang開發環境設定

[前言]

工欲善其事,必先利其器。

Golang是個跨作業系統(像是: Windows、Linux、Mac OS等等)與支援多CPU架構(x86、ARM64等等)的程式語言,換句話說,Golang的程式可以透過go command直接在眾多的作業系統與幾種不同的CPU架構下執行。

進一步來說,也可以把Golang程式編譯/或交叉編譯 到不同的作業系統與不同的CPU架構下成為執行檔案,即可直接執行。其內涵不了解沒有關係,這不防礙接下來安裝的步驟。


[安裝Golang]

我這邊只先舉Windows 10的安裝範例,對於Linux或是Mac的作業系統,網路上應該有很多安裝教學可以參考ㄡ~

首先請用點擊下列Golang官方下載網頁:

https://go.dev/dl/

[用Golang寫小遊戲教學] 第二章 基本型別、變數宣告與常數宣告

[前言]

了解"基本型別"與"變數宣告"是開始學習程式語言的第一步。我們其實不需要一次把所有的觀念與知識都學到位,這對於初學者或是小朋友來說太過於複雜,這裡就先介紹比較常見的部分。

這篇的教學還不需要用到本機上安裝的Go來執行,我們會用 The Go Playground (網頁版)來練習。


[變數宣告]

在電腦的世界中,每個程式都需要在記憶體中儲存一些資料,資料被儲存在特定的記憶體位置,變數只是為儲存資料的記憶體位置所取的一個方便的名稱。除了名稱外,每個變數還有關聯的型別。型別用來定義存放的資料是屬於何種型態,像是整數或是字串等等,在後面的段落將說明何為資料型別。

    我們先看一個例子: var myScore int = 100,並參考下圖

Thursday, December 23, 2021

[用Golang寫小遊戲教學] 第零章 大綱

[前言]

曾經有位朋友與我討論軟體工程師如何寫好程式(或是開發出好的軟體),我依稀記得是這樣回答:

"木匠製作椅子為例子來說,他(木匠)本身需要具備身為木匠的能力,例如: 鋸木、裁切、刨木、鑽孔、拋光、上漆等等功夫,接下來透過不斷的實作成品的鍛鍊,可以學習到製作越來越複雜的木工成品,當累積一段時間之後,會有自己的風格與設計架構對於擅長的木工項目。

同理可推,在軟體工程師的角度來說,是須要懂得電腦基本概念、了解作業系統的運行、與熟悉某種程式語言,這是基本功。在不斷的實作過程中累積出自己的實力,才有可能寫出好的程式。"

這樣的回答可能沒有完全說服那位朋友,是基本功對於一般人來說,會是個門檻與障礙。

對於像是不具備相關背景的大朋友們,可能會有心理障礙而不敢學習寫程式,擔心學習門檻很高或是學不會。對於小朋友們來說,過早就進入複雜的背景知識或是學習太多程式語言的細節,不只無法理解其內容,並且會快速遇到瓶頸而備受打擊,或是失去學習程式的動力與興趣。


[教學內容列表]

Friday, December 10, 2021

[用Golang寫小遊戲教學] 第三章 初探Ebiten (A dead simple 2D game library for Go)

[前言]

Ebiten 官方網站 (The official website)

用Golang寫小遊戲教學這一系列的內容,將會以一個基於Golang語言的 2D Game Library: Ebiten,對於製作小遊戲來說,感覺簡單又易於使用 。Ebiten 套件原作者是日本人,少量遊戲作品可在 wiki 中找到: https://github.com/hajimehoshi/ebiten/wiki/Works

很多使用範例可以學習: 

Wednesday, December 1, 2021

[GCC] Using GCC to create static and shared library ( .so / .a )

[轉貼&修改] https://blog.xuite.net/csiewap/cc/23626229-Using+GCC+to+create+static+and+shared+library+.so 

Library可分成三種,static、shared與dynamically loaded。


1. Static libraries

Static 程式庫用於靜態連結,簡單講是把一堆object檔用ar(archiver)

包裝集合起來,檔名以 `.a' 結尾。優點是執行效能通常會比後兩者快

而且因為是靜態連結,所以不易發生執行時找不到library或版本錯置而

無法執行的問題。缺點則是檔案較大,維護度較低;例如library如果發

現bug需要更新,那麼就必須重新連結執行檔。

1.1 編譯

編譯方式很簡單,先例用 `-c' 編出 object 檔,再用 ar 包起來即可。

hello.c 

#include
void hello(){ printf("Hello "); }

world.c 

#include
void world(){ printf("world."); }

mylib.h 

void hello();
void world();

$ gcc -c hello.c world.c /* 編出 hello.o 與 world.o */

$ ar rcs libmylib.a hello.o world.o /* 包成 limylib.a */

這樣就可以建出一個檔名為 libmylib.a 的檔。輸出的檔名其實沒有硬性規定,

但如果想要配合 gcc 的 '-l' 參數來連結,一定要以 `lib' 開頭,中間是你要

的library名稱,然後緊接著 `.a' 結尾。

1.2 使用

 main.c 

#include "mylib.h"

int main() {
    hello();
    world();
}

使用上就像與一般的 object 檔連結沒有差別。

$ gcc main.c libmylib.a

也可以配合 gcc 的 `-l' 參數使用

$ gcc main.c -L. -lmylib

`-Ldir' 參數用來指定要搜尋程式庫的目錄,`.' 表示搜尋現在所在的目錄。

通常預設會搜 /usr/lib 或 /lib 等目錄。

`-llibrary' 參數用來指定要連結的程式庫 ,'mylib' 表示要與mylib進行連結

,他會搜尋library名稱前加`lib'後接`.a'的檔案來連結。

$ ./a.out
Hello world.

此時可以用 nm <exe filename> 來顯示symbols (functions, etc.)
(
To see which symbols come from static libraries requires running nm against those libraries to get a list of the symbols (functions, etc.) in them, then comparing them to what your list of symbols from nm <exefilename>.)


2. Shared libraries

Shared library 會在程式執行起始時才被自動載入。因為程式庫與執行檔

是分離的,所以維護彈性較好。有兩點要注意,shared library是在程式起始

時就要被載入,而不是執行中用到才載入,而且在連結階段需要有該程式庫

才能進行連結

首先有一些名詞要弄懂,soname、real name與linker name

soname 用來表示是一個特定 library 的名稱,像是 libmylib.so.1

前面以 `lib' 開頭,接著是該 library 的名稱,然後是 `.so' ,接著

是版號,用來表名他的介面;如果介面改變時,就會增加版號來維護相容度。

real name 是實際放有library程式的檔案名稱,後面會再加上 minor 版號與

release 版號,像是 libmylib.so.1.0.0

一般來說,版號的改變規則是(印象中在 APress-Difinitive Guide to GCC中有

提到,但目前手邊沒這本書),最尾碼的release版號用於程式內容的修正,

介面完全沒有改變。中間的minor用於有新增加介面,但相舊介面沒改變,所以

與舊版本相容。最前面的version版號用於原介面有移除或改變,與舊版不相容

時。

linker name是用於連結時的名稱,是不含版號的 soname ,如: libmylib.so。

通常 linker name與 real name是用 ln 指到對應的 real name ,用來提供

彈性與維護性。


2.1 編譯

shared library的製作過程較複雜。

$ gcc -c -fPIC hello.c world.c

編譯時要加上 -fPIC 用來產生 position-independent code。也可以用 -fpic

參數。 (不太清楚差異,只知道 -fPIC 較通用於不同平台,但產生的code較大

,而且編譯速度較慢)。

$ gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.0.0 \

hello.o world.o

-shared 表示要編譯成 shared library

-Wl 用於參遞參數給linker,因此-soname與libmylib.so.1會被傳給linker處理。

-soname用來指名 soname 為 limylib.so.1

library會被輸出成libmylib.so.1.0.0 (也就是real name)

若不指定 soname 的話,在編譯結連後的執行檔會以連時的library檔名為

soname,並載入他。否則是載入soname指定的library檔案。

可以利用 objdump 來看 library 的 soname。

$ objdump -p libmylib.so | grep SONAME

SONAME libmylib.so.1

若不指名-soname參數的話,則library不會有這個欄位資料。

在編譯後再用 ln 來建立 soname 與 linker name 兩個檔案。

$ ln -s libmylib.so.1.0.0 libmylib.so

$ ln -s libmylib.so.1.0.0 libmylib.so.1


2.2 使用

與使用 static library 同。

$ gcc main.c libmylib.so

以上直接指定與 libmylib.so 連結。

或用

$ gcc main.c -L. -lmylib

linker會搜尋 libmylib.so 來進行連結。

如果目錄下同時有static與shared library的話,會以shared為主。

使用 -static 參數可以避免使用shared連結。

$ gcc main.c -static -L. -lmylib

此時可以用 ldd 看編譯出的執行檔與shared程式庫的相依性

$ldd a.out

linux-gate.so.1 => (0xffffe000)

libmylib.so.1 => not found

libc.so.6 => /lib/libc.so.6 (0xb7dd6000)

/lib/ld-linux.so.2 (0xb7f07000)

輸出結果顯示出該執行檔需要 libmylib.so.1 這個shared library。

會顯示 not found 因為沒指定該library所在的目錄,所找不到該library。

因為編譯時有指定-soname參數為 libmylib.so.1 的關係,所以該執行檔會

載入libmylib.so.1。否則以libmylib.so連結,執行檔則會變成要求載入

libmylib.so

$ ./a.out

./a.out: error while loading shared libraries: libmylib.so.1:

cannot open shared object file: No such file or directory

因為找不到 libmylib.so.1 所以無法執行程式。

有幾個方式可以處理。

a. 把 libmylib.so.1 安裝到系統的library目錄,如/usr/lib下

b. 設定 /etc/ld.so.conf ,加入一個新的library搜尋目錄,並執行ldconfig

更新快取

c. 設定 LD_LIBRARY_PATH 環境變數來搜尋library

這個例子是加入目前的目錄來搜尋要載作的library

$ LD_LIBRARY_PATH=. ./a.out

Hello world.


3. Dynamically loaded libraries

Dynamicaaly loaded libraries 才是像 windows 所用的 DLL ,在使用到

時才載入,編譯連結時不需要相關的library。動態載入庫常被用於像plug-ins

的應用。

3.1 使用方式

動態載入是透過一套 dl function來處理。

#include

void *dlopen(const char *filename, int flag);

開啟載入 filename 指定的 library。

void *dlsym(void *handle, const char *symbol);

取得 symbol 指定的symbol name在library被載入的記憶體位址。

int dlclose(void *handle);

關閉dlopen開啟的handle。

char *dlerror(void);

傳回最近所發生的錯誤訊息。

dltest.c

#include
#include
#include

int main() {
    void *handle;
    void (*f)();
    char *error;

    /* 開啟之前所撰寫的 libmylib.so 程式庫 */
    handle = dlopen("./libmylib.so", RTLD_LAZY);
    if( !handle ) {
        fputs( dlerror(), stderr);
        exit(1);
    }

    /* 取得 hello function 的 address */
    f = dlsym(handle, "hello");
    if(( error=dlerror())!=NULL) {
        fputs(error, stderr);
        exit(1);
    }
  /* 呼叫該 function */
    f();
    dlclose(handle);
}

編譯時要加上 -ldl 參數來與 dl library 連結

$ gcc dltest.c -ldl

結果會印出 Hello 字串

$ ./a.out

Hello

關於dl的詳細內容請參閱 man dlopen

--

參考資料:

Creating a shared and static library with the gnu compiler [gcc]

Program Library HOWTO

Makefile

GCC=gcc  
CFLAGS=-Wall   -ggdb   -fPIC  
#CFLAGS=  
   
all:   libfunc  
   
libfunc:func.o   func1.o  
    $(GCC)   -shared   -Wl,-soname,libfunc.so.1   -o  libfunc.so.1.1   $<  
    ln   -sf   libfunc.so.1.1   libfunc.so.1  
    ln   -sf   libfunc.so.1   libfunc.so  
   
%.o:%.c  
    $(GCC)   -c   $(CFLAGS)   -o   $@   $<  
   
clean:  
    rm   -fr   *.o  
    rm   -fr   *.so*