gd 公式マニュアル
オンラインのマニュアル・APIリファレンスはブラウザーの優先言語に合わせて英語・日本語を表示し、対象外の言語では英語を表示します。 言語ボタンまたは?lang=jaで日本語へ切り替えられ、選択は端末内に保存されます。
gdは何のための道具か
gdは、GDScriptで端末の道具、Webサイト、Web API、定期処理、データ処理を書くための単体コマンドです。 Godotを画面なしで小さく組んであり、project.godotを用意せずに.gd fileを一枚書いて実行できます。
PythonやNode.jsでscriptを書く感覚で始められ、必要になった時点で型検査、test、package、database、 Web server、単一実行体へ同じGDScriptのまま進めます。ゲームの画面や描画を作る用途にはGodot本家を使ってください。
Godot本家とあわせれば、アプリもフロントもserverも端末ツールも、一つの言語GDScriptで書けます。 同じscriptがmacOS、Linux、Windowsで動き、通信とdatabaseの待ちはほかの処理を止めません。 GDExtensionでC++と直接つながります。AI agentが書いて動かすことを前提に設計しています。
導入
対応環境はmacOS arm64/x86_64、Linux x86_64、Windows x86_64です。 ReleasesからOSに合うarchiveを取得し、 中のgdをPATHの通ったdirectoryへ置きます。gd --versionが版を表示すれば導入は完了です。 配布物のSHA-256は同梱のSHA256SUMSで照合できます。macOS版はDeveloper ID署名とAppleの公証を通しています。
sourceからbuildする場合はPython、uv、SCons、C/C++ compilerを用意し、bin/にできるgd.*.template_release.*を使います。 TLSは内蔵しているため、別のTLSライブラリは要りません。
git clone https://github.com/prog-sha/gd-cli.git
cd gd-cli
scons platform=macos target=template_release -j8
# Linux: platform=linuxbsd
# Windows: platform=windows windows_subsystem=console
クイックスタート
hello.gdを一枚作ります。設定fileやpackageは要りません。main()が入口で、返した整数がprocessの終了codeになります。
func main():
print("Hello, world")
return 0
gd hello.gd
実行せずに型と構文を調べるにはcheckを使います。
gd check hello.gd
用途から選ぶ
標準APIの入口はGD一つで、用途ごとの子を持ちます。scriptからはこの名前をそのまま書きます。
| やりたいこと | 入口 | 例 |
|---|
| file、文字、日時、HTTP client、非同期処理 | GD | GD.file.read_text("a.txt") |
| WebサイトとWeb API | GD.web | GD.web.app() |
| SQLiteまたはPostgreSQL | GD.database | GD.database.client() |
GD.database.postgresとGD.database.redisは、接続先固有の機能が必要なときに使う高度な入口です。
APIの調べ方
gd docに、scriptへ書く綴りをそのまま渡します。署名は実行体から作るため、実装と一致します。
gd doc # Show a short guide and entry points.
gd doc manual # Read the complete manual.
gd doc GD # List the standard modules.
gd doc GD.file # file API
gd doc GD.http.fetch # Inspect the returned HTTP response.
gd doc GD.web.app # Web application
gd doc SceneTree # Inspect a public engine class.
gd doc all # List public types.
戻り型のR、Err、GDWebRequestは名前だけで引きます。Node、SceneTree、TimerなどGodot由来の型は Godotのclass referenceも参照してください。 手引きの言語はLC_ALLまたはLANGがjaで始まるとき日本語、それ以外は英語です。 Web版はgd-cli.progsha.comにあり、日本語と英語を切り替えられます。
GDScriptの基本
掲載例では型名を繰り返しません。:=で代入する変数と、return 値, 失敗で返す成功値は型を推論します。 注釈を省いた引数は動的型です。型で境界を固定したい箇所だけ注釈を足せます。
引数とflag
script名の後ろに置いた引数はmain(argv)で受け取ります。--name=gdのようにgd自身のflagと紛らわしい引数は、 --の後ろへ置くとscriptへ渡ります。
func main(argv):
for arg in argv:
print(arg)
return 0
gd main.gd apple orange
gd main.gd -- --name=gd
flagとして解釈したいときはGD.cli.flags()を使います。--name gd、--name=gd、-name=gdのどの綴りも受けます。
func main(argv):
var flags := GD.cli.flags()
flags.flag_str("name", "world", "挨拶する相手")
var parsed := flags.parse(argv)
if not parsed.ok:
print(flags.usage())
return 1
print("Hello, " + flags.get_str("name"))
return 0
外のcommandを呼ぶ
外の道具はGD.cli.run()で呼びます。待つのは呼び出したGDScriptだけなので、gd serveのhandlerの中から呼んでも他のrequestは進みます。 --strictでは--allow-runが要ります。--allow-run=/usr/bin/gitのように相手を絞れます。
func main():
var got := GD.cli.run("git", ["rev-parse", "HEAD"])
if not got.ok:
return 1
print("code=", got.v["code"], " out=", got.v["output"])
return 0
第3引数のoptsで挙動を変えられます。
| 名前 | 既定 | 意味 |
|---|
timeout | 0 | 諦めるまでの秒数。0は無期限。越えると子を畳んでErr.TIMED_OUTを返す |
output | true | 出力を集める。falseなら親の標準入出力へ直結し、集めない |
開発中のcommand
gd check main.gd # Check syntax and types without execution.
gd fmt main.gd # Format the source consistently.
gd test # Discover and run *_test.gd files.
gd --watch main.gd # Restart after each source save.
gd eval 'print(1 + 1)' # Evaluate one expression.
gd repl # Start an interactive session.
値と失敗
失敗しうる関数は、例外を投げる代わりに「成功値と失敗」の二つの値を返します。 受け取る側はvar 値, e :=の形で両方を受け、eがnullでなければ失敗です。
func main():
var text, e := GD.file.read_text("note.txt")
if e:
print(e.text())
return 1
print(text)
return 0
失敗を短く扱う
毎回if eを書く代わりに、呼出しの末尾へ?を付けると、失敗をそのまま呼出し元へ返して成功値だけが残ります。 ?を使う関数は、自分もreturn 値, 失敗で成功値と失敗を返します。
func title(path):
var text := GD.file.read_text(path)?
return text.strip_edges(), null
func main():
var text, e := title("note.txt")
if e:
print(e.note("題名を読む").text())
return 1
print(text)
return 0
| 書き方 | 意味 |
|---|
var value, e := call() | 成功値と失敗を分けて受ける |
return 値, null / return null, 失敗 | 成功または失敗を返す。成功値の型は値から推論する |
call()? | 失敗なら呼出し元へそのまま返す |
call()! | 失敗なら理由を表示して、programをその場で止める(終了codeは1)。試作やtest向き。gd serveではそのhandlerだけが失敗する |
e.note("目的") | 失敗に作業の文脈を足す。表示は「目的: 元の理由」の形になる |
e.kind | Err.NOT_FOUND、Err.INVALID_DATAなどの種類。分岐に使う |
Err.err("理由", Err.NOT_FOUND) | 自分で失敗を作る |
失敗を呼出し元へ渡さないmain()ではvar 値, e :=か!で受けます。
file操作の失敗ではe.infoにop、path、source、source_codeが入ります。 renameはpathの代わりにoldとnewを持ちます。sourceはposix、win32、engineのいずれかです。 NotFoundなど意味が確定した場合だけkindが付き、未知のI/O失敗はErr.NONEのまま元情報を保ちます。
戻り値の規則
型を書かなくても動きます。型を書く場合と細部の規則は次の通りです。
- 戻り型は
-> int, Errのように成功型一つとErrの二つです。実行時の型はRで、-> Rや省略もできます。
- カンマ戻りは必ず二値です。末尾は
Err型の値か成功時のnullに限ります。文字列は末尾に直接返せないのでErr.err(reason)で包みます。
- 失敗を入れる変数は
var e: Err = ...かvar e := Err.err(...)で型を固定します。型が変わりうるvar e = ...は末尾に使えません。
- 複数のdataは
return [1, 0.0, ""], nullのように配列や辞書一つへまとめます。return null, nullはnullを成功値として返します。
Rの分解はvar value, e := call()の二つの名前に固定です。var a, b, c := 1, "a", 0.0のように式を並べる宣言は別物で、個数の制限はなく、各名前を対応する式から推論します。新しい名前を一つ以上含めば、その関数で見えている変数(外側のblockの変数を含む)にも代入でき、右辺は代入の前に全て評価します。定数、引数、lambdaが外から取り込んだ変数には代入できません。
- 型注釈した戻り値に
, Errが無い関数で?を書くとThe "?" operator needs a function returning "R" or "Err".になります。
-> int, Errでreturn R.ok("a")と書くとcompile errorです。型が動的なら実行時に検査します。
- 型付きのArrayやDictionaryを成功値にするときは、元のcontainerにも同じ要素型を付けます。
- 成功値と失敗を返す関数は、全ての経路で
returnします。?で伝播するだけの関数も最後にreturn null, nullを書きます。
- 戻り型を
-> intのように一つだけ書いた関数には、カンマ戻りを書けません。-> int, Errと書きます。
- lambdaにはカンマ戻りを書けません。
return R.ok(値)とreturn R.err(理由)を使います。
Rで持ち運ぶ
値と失敗を一つの値として持ち運びたいときはRを使います。okで成否、vで成功値、eで失敗を読みます。 R.ok(値)とR.err(理由, 種類, 部分値)で作ります。Webのhandlerやdatabaseのtransactionは、このRを返す形でも書けます。
func find(items, want):
for item in items:
if item == want:
return R.ok(item)
return R.err("not found: " + want, Err.NOT_FOUND)
func main():
var got = find(["a", "b"], "c")
if not got.ok:
print(got.e.text())
return 1
print(got.v)
return 0
R.ok()の成功値はnullで、intの0にはなりません。途中まで進むI/O APIは、失敗したときも完了した量を部分値としてvに残します。 note()は部分値を保ち、v_or(代替値)は失敗なら代替値を返します。 ?で伝播するとき、部分値が呼出し元の成功型に合わなければ部分値だけを捨て、失敗の理由と種類は保ちます。
待つ処理と同時実行
HTTP、database、GD.net、fileなどの待つmethodは、普通の関数呼出しとして書けます。 待つのは呼び出したGDScriptだけで、ほかの通信やtimerは進みます。
func main():
var res := GD.http.fetch("https://example.com/")
print(res.status)
return 0
複数の処理を同時に始めたいときは、末尾が_asyncの版とGD.async.all()を使います。
func main():
var got = await GD.async.all([
GD.http.fetch_async.bind("https://example.com/a"),
GD.http.fetch_async.bind("https://example.com/b"),
])
for res in got:
print(res.status)
return 0
| 入口 | 用途 |
|---|
名前_async() | 処理を始めてSignalを返す。awaitすると通常名と同じ結果になる |
GD.async.all(list) | CallableとSignalを受け取り、全部の結果を入力順に返す。無効な入力は対応する欄がエラーになる |
GD.async.spawn(fn) | GDScriptの関数を裏で走らせる。main()が返った後も動く |
GD.async.sleep(sec) | 指定秒だけ待つ |
all()へはSignalよりCallableを渡してください。開始前に完了を購読するため、先に終わった結果を取りこぼしません。
:=で保存したSignalは完了時の型も保持します。異なる型や型不明のSignalへの再代入は拒否します。 実行時に型を決める場合は受け側をSignalと明示し、完了値にも必要な型を付けます(例: var result: R = await pending)。 spawn()はCPU処理を別threadへ移す機能ではありません。長いGDScriptは自動的にほかの処理へ実行権を譲りますが、 native methodの内部は中断しないため、大きな入力を標準moduleへ渡すときは_asyncの版を使います。 待つmethodを呼べるのは、GDScriptから呼ばれた関数の中だけです。Array.map()のcallback、_init()、member変数の初期化、_to_string()のようにnativeから呼び返される関数の中で呼ぶとerrorになるので、そこでは_asyncの版をawaitするか、coroutineから呼びます。
チュートリアル: SQLiteを使うメモAPI
ここまでの知識で、JSONを受けてSQLiteへ保存する小さなAPIを一枚のscriptで作ります。 できあがるのは、入力検査とSQLのparameter bindを備え、権限を絞って起動する開発用serverです。
1. 作業directoryを作る
mkdir notes-api
cd notes-api
2. APIを書く
次をmain.gdとして保存します。
# Store notes in an embedded database and expose a JSON API.
extends RefCounted
const PORT := 18080 # Development listener port on loopback.
const DB_PATH := "user://notes.sqlite3" # Writable storage isolated per user.
var app := GD.web.app()
var db := GD.database.client()
# Return notes as JSON in newest-first order.
func list_notes(_req):
var got := db.query("SELECT id, title FROM notes ORDER BY id DESC")?
return GD.web.json(got.rows), null
# Save a validated title and return the created row.
func add_note(req):
var body := req.valid("body")
var made := db.query(
"INSERT INTO notes(title) VALUES($1) RETURNING id, title",
[body.title]
)?
return GD.web.json(made.rows[0], 201), null
# Prepare the database and routes, then listen on loopback.
func main():
db.open({"driver": "sqlite", "path": DB_PATH})?
db.query("CREATE TABLE IF NOT EXISTS notes(id INTEGER PRIMARY KEY, title TEXT NOT NULL)")?
app.route("GET", "/notes", list_notes)
app.route("POST", "/notes", add_note, [GD.web.json_body(GD.web.object_rule({
"title": GD.web.text_rule({"min": 1, "max": 120}),
}))])
app.listen(PORT, "127.0.0.1")?
print("listening on http://127.0.0.1:%d" % PORT)
return 0, null
上から順に読みます。
appはrouter、dbはdatabase接続です。main()が返った後もserverが動き続けられるよう、両方ともscriptの変数として持ちます。
main()はまずSQLiteを開き、表を作ります。DB_PATHのuser://は、gdが利用者ごとに用意する書込み領域です。
app.route()に、HTTP method、path、そのときに呼ぶ関数(handler)を登録します。
- handlerは
GDWebRequestを受け取り、GD.web.json()で返事を作ります。途中の?は失敗をserverへ返し、状態番号500などになります。
- POSTには
GD.web.json_body()を付けています。本文がruleに合うときだけhandlerが呼ばれ、通った値がreq.valid("body")に入ります。
- SQLの値は
$1へbindします。文字列連結でSQLを組み立てません。
3. 権限を絞って起動する
未確認のscriptや外へ公開するserverは、権限を既定で拒否する--strictで実行します。ここでは待受先をloopbackの一つのportに絞ります。 serveはmain()が返った後もprocessを残すcommandで、serverにはこれを使います。
gd check main.gd
gd --strict --allow-net=127.0.0.1:18080 serve main.gd
4. 別の端末から使う
curl -s -X POST http://127.0.0.1:18080/notes \
-H 'Content-Type: application/json' \
-d '{"title":"gdを試す"}'
curl -s http://127.0.0.1:18080/notes
最初はstatus 201と作成した一件、次は保存済みの配列が返ります。空の題名、120文字を超える題名、 JSONでない本文は400で拒否されます。止めるときは起動した端末でCtrl-Cを押します。
公開環境ではこのprocessをloopbackのままTLS reverse proxyの後ろへ置き、異常終了耐性が必要な保存先は PostgreSQLへ切り替えます。接続情報はsourceへ書かず、許可した環境変数から読みます。
権限
gdには二つの実行方式があります。
| 方式 | 向く場面 | 制限 |
|---|
| 通常実行 | 信頼したsourceを開発中に動かす | fileもnetworkも制限しない |
--strict | 未確認のscript、公開server | res://と絶対pathはread-only。network、環境変数、子process、native extension、system情報を既定で拒否 |
--strictでは、使うものを挙げて起動します。
gd --strict \
--mount store=/srv/app:rw \
--allow-net=db.example.com:5432 \
--allow-env=DATABASE_URL \
main.gd
| 指定 | 許すもの |
|---|
--mount name=path:r / --mount name=path:rw | 名前付きdirectoryのreadまたはread/write |
--allow-net=host:port,... | 接続と待受。値を省くと全て |
--allow-env=name,... | 環境変数 |
--allow-run=command,... | 子process |
--allow-ext=path,... | scriptが実行中に読むnative extension |
--allow-sys=item,... | 機種とsystem情報 |
--deny-* | 対応するallowより優先する拒否 |
-A | file以外を全て許す。開発中の一時的な利用向け |
fileの置き場
scriptから見えるfileの置き場は次の4種類です。置き場の名前をpathの先頭に書くか、絶対pathをそのまま書きます。
| 書き方 | 指す場所 | strictでの扱い |
|---|
res://a.txt | scriptを起動したdirectory | read-only |
user://a.txt | gdが利用者ごとに用意する書込み領域 | read/write |
store://a.txt | --mount store=/srv/app:rwで付けた名前 | 指定した権限 |
/etc/hosts | 機械上のその場所 | read-only |
res://より上へ遡る相対pathは、どちらの方式でも拒否します。
--mountと絶対pathはLinuxとmacOS用です。Windowsでは拒否するので、fileはres://かuser://へ置いてください。
- mount名に使えるのは小文字の英数字と
-です。res、user、uid、pipe、local、libgodot、tcp、unix、http、https、file、data、cacheは予約済みで選べません。
networkとextensionの許可
--allow-netのlocalhost:8080は、同じportのIPv4 loopback 127.0.0.0/8とIPv6 ::1も表します。
*.example.com:443はその下位hostを許します。
- native extensionは同じprocessで動くため、信頼できるものに限ってください。
serveとSceneTree
gd serveはSceneTreeを作らない常駐用の実行方式です。通信、timer、await、自作Signal、GD.async.sleep()、 ツリー外Nodeのqueue_free()は動きます。仕事が無ければ次の期限か通信の通知まで眠るため、周期の調整は要りません。
| やりたいこと | 方法 |
|---|
| Web serverや定期処理を常駐させる | gd serve main.gd |
Nodeの_process()、_physics_process()、process_frame、SceneTreeTimer、高水準multiplayerを使う | serveを付けない通常実行 |
| SceneTreeやMainLoopを継承したscriptを動かす | serveを付けない通常実行 |
| SceneTreeが紛れ込んでいないか開発中に調べる | gd --no-scene-tree --allow-net serve app.gd |
| 複数processで待ち受ける | --workers=<n>または--workers=auto。nは1以上の整数 |
serveではNodeを継承しただけのscriptはツリーへ追加されません。 --no-scene-treeはSceneTreeが作られた時点で診断を出し、終了code 1で止まります。--watchと--workersの子にも引き継がれます。 通常実行は暗黙にSceneTreeを作るため、この旗を付けると失敗します。
TCPとUDP
低水準の通信にはGD.netを使います。Godot本家の低水準型も互換用に残っていますが、新しいcodeはGD.netで書きます。
func echo():
var listener := GD.net.listen_tcp("127.0.0.1", 8080)?
var conn := listener.accept()?
var data := conn.read(65536)?
conn.write(data)?
conn.close()
return 0, null
GDTCPConnのreadとwriteは別々の列なので、複数のGDScriptから同時に呼べます。
- 期限methodは今からの秒数を設定し、0で解除します。
close()は未完了のread/writeをErr.INTERRUPTEDで起こします。listenerの受付待ちも同じです。
- 接続は名前解決で得たIPv4とIPv6の候補を順に試し、成功した一本だけを残します。全体の
timeoutは延びません。
TLS
TLSはGD.net.dial_tls(host, port, opts)で開きます。既定で証明書の鎖とhost名を検証し、失敗しても平文へ戻りません。 戻り値はTCPと同じGDTCPConnです。
opts | 意味 |
|---|
timeout | 接続と握手を合わせた期限の秒 |
ca_file | 私設CA。環境変数の設定より優先する |
cert_file、key_file | client認証。許可されたmount内のfileを対で指定する |
server_name | 証明書を照合する宛名を接続先と別にする |
next_protos | ALPN名の配列。1名は1–255 byte、全体で65535 byteまで |
insecure_skip_verify | 検証を省く。検証不要と判断できる試験時だけ使う |
交渉結果はconnection_state()のnegotiated_protocolとversionで読めます。TLS1.2は771、TLS1.3は772です。
信頼するCAは、未指定ならmacOSとWindowsではOSの信頼設定、Linuxではsystem CA bundleです。 起動前にSSL_CERT_FILEまたはSSL_CERT_DIRを設定すると、どのOSでも指定したCAを使います。 directoryの区切りはUnixで:、Windowsで;です。
serverがclient証明書を求めるときは、app.listen_tls(port, cert, key, host, opts)のoptsへclient_ca(信頼CA bundle)とclient_authを渡します。 client_caが未指定ならsystemの信頼設定を使います。
client_auth | 動作 |
|---|
none | 証明書を要求しない |
request | 任意提示。検証しない |
require | 提示だけ必須。検証しない |
verify_if_given | 提示された場合だけ検証する |
require_and_verify | 検証済みの証明書を必須にする |
UDPと名前解決
GD.net.listen_udp()はGDUDPPacketConnを返します。read_from()はdata、host、port、truncatedを持つ辞書を返します。 write_to()のhostにはGD.net.resolve()で解決したIP addressを渡します。packetは結合されません。 buffer=0(既定)はOSの受信bufferをそのまま使い、正の値を指定した場合だけ変更を要求します。
GD.net.resolve()はOSが選んだ先頭のaddressを一つ返します。名前のcacheは持ちません。 GD.net.local_addresses()は機械のaddress一覧を返し、空の一覧とOSの失敗を区別します。 失敗のe.infoにはsyscall、source、source_codeが入ります。
fileとdata
GD.fileでfileの読み書きとpath操作をします。起動したdirectoryがres://で、絶対pathも書けます。 strictで外部directoryへ書くときは--mount store=/srv/app:rwで付けた名前をstore://users.csvのように書きます。
func main():
var rows := GD.data.csv_objects(GD.file.read_text("store://users.csv")?)?
GD.file.write_text("store://users.json", JSON.stringify(rows))?
return 0, null
fileの操作は、通常名でも呼び出したGDScriptだけを待たせます。Web serverのhandlerから読んでも他のrequestは進みます。 複数の操作を同時に始めるときだけ、末尾が_asyncの版を使います。
func handler(_req):
var body := GD.file.read_text("store://big.json")
if not body.ok:
return GD.web.text("読めません", 500)
return GD.web.text(body.v)
compileで同梱したfileも、同じAPIで読取、列挙、static配信ができます。
大きなfileを読む
全量をmemoryへ置かず読むときはGD.file.open(path, mode)でGDFileStreamを開きます。 modeはread、write、append、read_writeで、使い終えたらclose()を呼びます。
| method | 動作 |
|---|
read(max) | 最大max byteを返す。少なく返ることがある。空の成功値がEOF |
write(bytes) | 全て書いてbyte数を返す。途中で失敗してもR.vに書込み済みbyte数が残る |
同じstreamの操作は受付順、別のstreamは並列に進みます。appendはseekの後も常に末尾へ書きます。 read_bytes()も途中で失敗したときは取得済みのbyte列をR.vに残します。 read_text()はStringに収まらない大きさの入力を切り詰めずエラーにするので、大きなfileはbyte列かstreamで扱います。
同時に更新されるfile
複数のprocessが同じfileを更新するときはGD.file.replace_text(path, old, body)を使います。 読み取ったoldと現在の内容が同じときだけ置き換えるため、並行編集を黙って上書きしません。新規作成ではoldにnullを渡します。
data形式の入口
| 用途 | 入口 |
|---|
CSV、TOML、YAML、JSONL、JSONC、XML、INI、TAR、front matter、.envのfileを読む | GD.file.read_csv(path)など |
| 同じ形式のmemory上の変換、JSON、codec、hash、HMAC、PBKDF2、HKDF、byte列 | GD.data |
| UUIDとULID | GD.id |
| 日時の変換と計算 | GD.time |
| 文字の整形と比較 | GD.text |
| HTML entity、tag、gdhtml(Mustache構文のマイクロテンプレート) | GD.html |
| flagと環境変数 | GD.cli |
| 配列と辞書の操作 | GD.collection |
| 数学の特殊値とbit演算 | GD.math |
| versionの比較 | GD.version |
| 端末とfileへのlog | GD.log |
| testの検査 | GD.test |
環境変数と.envは、読む対象で入口が分かれます。
| 読む対象 | 入口 |
|---|
| processの環境変数 | GD.cli.env(name, fallback)、GD.cli.require_env(name)。strictでは--allow-envが要る |
.env file | GD.file.read_env(path)。fileを読んで辞書にする |
| dotenv形式の文字列 | GD.data.env(src)とGD.data.to_env(data)。memory上で辞書と変換する |
memory上の変換は通常名がその場で計算し、_asyncの版は別のthreadで計算します。大きな入力には_asyncを使います。 正確な一覧はgd doc GD.fileとgd doc GD.dataで引けます。形式ごとの検査と上限は、APIリファレンスの各入口の説明にあります。
GD.collectionのCallableを使う操作は、約1 msごとにほかの処理へ実行権を譲ります。 GD.logの各呼出しは書込み完了まで待ち、本文を切り捨てません。失敗は戻り値のRで確認でき、GD.log.flush()でそれ以前の出力完了を待てます。
JSONの規則
値はGD.data.json_encode(value)でJSON byte列にし、外から受けたbyte列はGD.data.json_decode(bytes)で読みます。 どちらも成功値とErrを返します。GDWebRequest.json()、GDHTTPResponse.json()、JSONLの各行も同じ規則です。
- 不正UTF-8、重複名、非有限数、非対応型、循環参照は、曖昧な値へ変えず失敗にします。
- signed 64-bitに収まる整数は
intのまま戻り、小数、指数、範囲外だけがfloatになります。文字列とキーの\u0000は保持します。
- 署名やcache keyのように同じ値から同じbyte列が必要なときは
{"deterministic": true}を指定します。
- 設定は
deterministicとescape_htmlがbool、max_bytesとmax_depthがintです。不正な型はErr.INVALID_DATA、上限超過はErr.LIMITEDです。
json_encode_async()が終わるまで、入力のArray・Dictionaryとその子要素を変更しないでください。設定の辞書は開始時に複製されます。
hashと鍵導出
GD.dataはSHA-1、SHA-224/256/384/512、SHA3-224/256/384/512を返します。HMAC、PBKDF2、HKDFでは sha1、sha224、sha256、sha384、sha512、sha3-224、sha3-256、sha3-384、sha3-512から方式を選べます。 PBKDF2とHKDFは出力長を指定でき、不正な方式、反復回数、出力長はRの失敗として返します。
threadの上限
GD.async.set_max_threads(max)はgdが管理するOS threadの上限を設定し、以前の値を返します。既定は10000です。 上限を越えるとprocessが終了します。現在数より小さい値への変更も終了します。外部libraryが直接作るthreadは数えません。
Webフレームワーク
GD.web.app()が返すrouterに、route、静的file、雛形、middlewareを登録します。HTMLを返すWebサイトも、JSONを返すWeb APIも同じ形で作ります。 まずHTMLを一枚返すsiteから始めます。
var app := GD.web.app()
func home(_req):
return GD.web.html("<h1>gd</h1><p>hello</p>"), null
func hello(req):
return GD.web.json({"message": "hello", "ip": req.ip}), null
func main():
app.static("/assets", "res://public")
app.route("GET", "/", home)
app.route("GET", "/api/hello", hello)
app.listen(8080, "127.0.0.1")!
return 0
gd --strict --allow-net=127.0.0.1:8080 serve main.gd
serveはmain()が返ってもprocessを終わらせないcommandです。gd main.gdで実行すると、待受けを始めた直後にprocessごと終わります。 起動しても待受けの合図は出ないので、応答の確認はbrowserやcurlで接続して行います。
routeと返事
route(method, pattern, handler)でHTTP methodとpathをhandlerへ結びます。patternの:nameはreq.params["name"]に入ります。 handlerはGDWebRequestを受け取ります。本文はreq.read()、bytes()、text()、json()、save()で必要な分だけ読みます。 HTMLのformから届く本文はGD.http.decode_query(req.text()?)で辞書にします。
handlerが返した値が返事になります。
| 返した値 | 返事 |
|---|
GD.web.html(body)、GD.web.view(path, data) | HTML |
GD.web.json(data) | JSON |
GD.web.text(body)、GD.web.bytes(body, type) | text、任意の媒体型 |
GD.web.stream(producer) | 少しずつ書く本文。「Webの運用と高度な機能」を参照 |
GD.web.redirect(to) | 302。toは同じsite内のpathに限り、他所へ送るときはawayをtrueにする |
GD.web.not_found() | 404 |
| 文字列 | text/plainの200 |
bodyを持たない辞書 | JSONの200 |
null | 204 |
失敗のRまたはErr | 種類に応じた状態番号。Err.NOT_FOUNDは404、Err.INVALID_DATAは400、他は500 |
ハンドラとmiddlewareは、awaitの後も含めてSignalを返せます。完了時の引数が0個ならnull、1個ならその値、複数ならArrayとして処理を再開します。利用できないSignalはエラーハンドラへ渡します。要求終了やapp停止時には待機中の購読を解除します。
- text/htmlは第2引数、bytesは媒体型の後の引数で状態番号を変えられます。
GD.web.header(reply, name, value)で返事にheaderを足します。
GD.web.guard(reply)でX-Content-Type-Options、X-Frame-Options、Content-Security-Policyなどの防御headerをまとめて足します。
- 失敗の理由は既定では本文に出しません。開発中に
app.show_errors(true)とした間だけ出します。
req.pathは各segmentを一度だけ復号したpath、req.targetはpercent escapeとqueryを保った原文です。%2Fは経路の区切りになりません。
- percent escapeを復号した結果が正しいUTF-8でない、または制御文字を含む要求は400を返します。
GD.web.json()やview()へ渡した値は、返事を送り終えるまで変更しないでください。
routerには次も登録できます。
| 登録 | 用途 |
|---|
app.static("/assets", "res://public") | prefix以下のGETをdirectoryのfileで返す。媒体型は拡張子から決め、directoryの外は返さない。/のindexはrouteで書く |
app.group("/api", [middleware]) | 共通prefixとmiddlewareを持つroute group。返り値にroute()とuse()がある |
app.fallback(handler) | どのrouteにも一致しない要求。404頁をここで返す |
app.on_error(handler) | handlerが失敗を返したときの返事 |
app.after(handler) | 返事を送る前の加工。func(req, reply)で受け、headerを足して返す |
middleware
middlewareは、handlerの前に呼ばれる関数です。GDWebRequestを受け取り、nullを返すと次へ進み、返事を返すとそこで止まります。 handle(req)を持つobjectも使えます。後段へ渡す値はreq.keep(name, value)で置き、req.kept(name)で読みます。
| 登録 | 掛かる範囲 |
|---|
app.pre(mw) | route選択の前。全要求 |
app.use(mw) | route選択の後。全route。req.paramsを読める |
group.use(mw) | そのgroupのroute |
app.route(method, pattern, handler, [mw]) | そのrouteだけ |
入力検査もmiddlewareです。GD.web.json_body(rule)、GD.web.query(rule)、GD.web.params(rule)が本文、query、pathの値を検査し、 通った値をreq.valid("body")、req.valid("query")、req.valid("params")に入れます。 ruleはGD.web.text_rule()、int_rule()、number_rule()、bool_rule()、list_rule()、object_rule()で組み、 GD.web.optional()とGD.web.one_of()で省略と選択肢を表します。queryとparamsの値は文字列なのでtext_rule()で検査し、必要ならto_int()で変換します。
var app := GD.web.app()
func show(req):
var params := req.valid("params")
return GD.web.json({"id": params.id}), null
func main():
app.route("GET", "/posts/:id", show, [GD.web.params(GD.web.object_rule({"id": GD.web.text_rule({"min": 1, "max": 20})}))])
app.listen(8080)!
return 0
組込みのmiddlewareはGD.web.sessions()、GD.web.csrf()、GD.web.jwt()、GD.web.rate()です。認証の節で使います。
HTML雛形
頁が増えてきたらHTMLを雛形fileへ出し、GD.web.view(path, data)で描画します。 雛形はgdhtml(Mustache構文のマイクロテンプレート)で、{{name}}、{{{html}}}、#if、#unless、#each、 #with、else、{{> header}}を扱います。views/page.htmlから{{> header}}を使うと、 同じ階層のviews/partials/header.htmlを読みます。
<!-- views/page.html -->
{{> header}}
<main><h1>{{title}}</h1></main>
<!-- views/partials/header.html -->
<header><a href="/">gd app</a></header>
func page(_req):
return GD.web.view("views/page.html", {"title": "Top"}), null
二重括弧の値は、置かれた位置から文脈を判定してescapeします。雛形の作者を信頼し、差し込む値を信頼しない前提です。
| 文脈 | 扱い |
|---|
| HTML本文、引用・未引用属性、属性名 | HTML escape |
href="{{url}}" | 相対URLとhttp、https、mailtoを通す。data-hrefも同じ |
href="/work/{{path}}"、href="/?q={{query}}" | pathは区切りを保って正規化、queryはpercent escape |
onclick、script本文 | JSON化し、application/jsonでも</script>が構造を壊さない形にする |
style | 安全な単独CSS値とCSS文字列・URLを通す |
| 危険なURL、srcset、CSS値、属性名 | 画面全体を失敗させず、#ZgdunsafeZまたはZgdunsafeZへ置き換える |
- 三重括弧
{{{html}}}はescapeしない唯一の入口で、HTML本文以外では使えません。固定HTMLか十分に検査済みの値だけを渡してください。
- 「検査済み」の印を付けて二重括弧のescapeを省く方法はありません。
- 分岐の両側や
eachの反復が異なる文脈で終わる雛形、閉じていないtag、曖昧なURLやJavaScript文脈は描画の失敗になります。
- 雛形の大きさに固定上限はありません。再帰する部品の深さだけは100000までです。
- 描画が終わるまで、渡した辞書を変更しないでください。
同じ雛形を何度も描画するserverでは、起動時にGD.html.template(source, partials)?で一度だけ解析し、 返った値のexecute(data)?を各要求から呼びます。解析結果は不変で、複数の要求から同時に使えます。 execute_bytes(data)?はUTF-8のbyte列を直接作るので、GD.web.bytes(body, "text/html; charset=utf-8")でそのまま返せます。
認証とCSRF
loginの状態はGD.web.sessions()で持ちます。issue(value)でsession IDを作り、cookie(id)の値をSet-Cookieで返します。 同じstoreをmiddlewareとして付けたrouteでは、cookieのIDに対応する値がreq.kept("user")に入り、無ければ401になります。
var app := GD.web.app()
var sessions := GD.web.sessions()
func login(req):
var form := GD.http.decode_query(req.text()?)?
var user := str(form.get("user", ""))
if user.is_empty():
return GD.web.text("user is required", 400), null
var reply := GD.web.redirect("/me")
return GD.web.header(reply, "Set-Cookie", sessions.cookie(sessions.issue(user))), null
func me(req):
return GD.web.text("hello, " + str(req.kept("user"))), null
func main():
app.route("POST", "/login", login)
app.route("GET", "/me", me, [sessions])
app.listen(8080)!
return 0
cookie(id)はSecureとHttpOnly付きで作ります。TLSなしの開発中に届かない場合はcookie(id, false)にします。 logoutはdrop(id)とclear_cookie()で行います。sessionはprocess内で持つため、--workersで複数processにするときはJWTか外部の保存先を使います。
cookieで認証する書き込み経路にはGD.web.csrf()を付けます。GET、HEAD、OPTIONS以外はBrowserの Sec-Fetch-Site: same-originが必要です。古いBrowserやBrowser以外のclientも受ける場合に GD.web.csrf({"allow_missing": true})を選び、別のtoken検証を組み合わせてください。
var app := GD.web.app()
var sessions := GD.web.sessions()
func save_email(_r):
return "saved"
func main():
app.route("POST", "/account/email", save_email, [GD.web.csrf(), sessions])
app.listen(8080)!
return 0
JWTをlogin sessionに使う場合は、password変更やlogoutで既発行tokenを失効させます。 checkは署名と標準claimの検証後に呼ばれ、trueを返したときだけ認証を通します。 例えばtokenへ利用者のverを入れ、password変更時に保存済みversionを増やします。 複数workerでは各processの辞書でなく、共有DBから同期したcacheなどで照合します。
func token_auth(key, versions):
return GD.web.jwt(key, {"check": func(claims):
return versions.get(claims.get("sub", ""), -1) == claims.get("ver", -2)
})
reverse proxyの後ろでIP単位に制限するときは、そのproxyのIPまたはCIDRをtrusted_proxiesへ明示します。 gdはX-Forwarded-Forの右端から信頼済みproxyを除き、最初の未信頼IPをkeyにします。 未指定のときと未信頼の接続元からのX-Forwarded-Forは無視するため、client自身によるIP偽装を許しません。 IPv4とIPv4-mapped IPv6は別物として照合するので、mapped addressを信頼する場合はIPv6のCIDRを指定します。zone付きのproxy設定は拒否します。
var per_ip := GD.web.rate({"limit": 60, "trusted_proxies": ["127.0.0.1", "172.18.0.0/16"]})
停止
終了待ちはapp.shutdown(context)を使います。新規受付とkeep-aliveを止め、処理中requestの完了を待ちます。 期限を越えた場合はErr.TIMED_OUTを返しますが、処理中requestは強制終了しません。 直ちに全接続を閉じる必要がある場合にapp.stop()を使います。
func close(app):
var context := GD.async.context().with_timeout(10.0)
var stopped := app.shutdown(context)
if not stopped.ok:
app.stop()
handlerではreq.contextからrequestの完了と切断を受け取れます。 with_cancel()とwith_timeout()は親を変更せず子のcontextを返し、親の打ち切りは子へ伝わります。 HTTP、database、processなどの待ちを打ち切れるようにするには、contextを先頭に渡してwith_context()で包みます。 処理が先に終わればその結果を返し、contextが先に終われば処理を取り消します。
func load(req, db):
var result = await GD.async.with_context(req.context, db.query_async("SELECT * FROM posts"))
return result
Webの運用と高度な機能
上限と大きなupload
大きな本文や長いhandlerを扱うときは、待受前にlimits()で上限を明示します。
func main():
var limited_app := GD.web.app()
limited_app.limits({"header_bytes": 1048576, "header_values": 500, "header_timeout": 15.0, "body_timeout": 10.0, "job_timeout": 30.0, "jobs": 128})
return 0
1 GBのZIPを受ける場合は要求ごとの上限を置き、書込み可能なmountへ逐次保存します。
func main():
var app := GD.web.app()
app.limits({"body_timeout": 600.0})
app.route("POST", "/upload", func(req):
req.limit(1000 * 1000 * 1000)
req.save("uploads://package.zip")?
return GD.web.text("saved")
)
return 0 if app.listen(8080, "127.0.0.1").ok else 1
gd --strict --allow-net=127.0.0.1:8080 --mount=uploads=/srv/uploads:rw serve main.gd
本文とmemoryの扱いは次の通りです。
| 対象 | 扱い |
|---|
| request body | 既定上限なし。見出しの後ですぐhandlerを呼び、本文はhandlerが読んだ分だけ接続から読む |
read()、save() | 本文を逐次読む。read()の空の成功値はEOF。save()は本文全体をmemoryへ置かない |
bytes()、text()、json() | 残りの本文全体をmemoryへ読む。大容量にはsave()を使う。text()はStringに収まる大きさまで |
req.limit(bytes) | 要求ごとの本文上限。超過は本文を読んだ操作へ失敗として返る |
| request header | 既定1 MiB。行数はheader_valuesを指定した場合だけ制限。trailerは4096 byte |
| HTTP clientのresponse header | 10 MiBまで |
| 遅い接続 | その接続だけを待たせ、別の接続を巻き込まない |
| 返事の追加header | 件数と全体量の固定上限なし。不正な名前と値だけを落とす |
| sessionとrate limit | process内で共有し、--workers間では共有しない。共有が必要ならDBなど外部の保存先を使う |
| session値 | 文字列と整数の識別子。保持件数はtotalとper_userで設定 |
| HS256 JWT | keyは32 byte以上。JSONと署名の妥当性を検査 |
| rate limitのkey | 保持件数はkeysで設定 |
| HTTP状態番号 | 100..999。範囲外は500として送る |
| port | 待受とGD.net.free_port()の探索開始は0を許し、接続先とis_free()は1..65535 |
| 問い合わせ文字列 | GD.http.decode_query()は素のsemicolonと壊れたpercent escapeを失敗として返す |
少しずつ返す本文
GD.web.stream(producer, length=-1, type="application/octet-stream", status=200)は、producer(writer)がGDWebWriterへ書いた分だけ送ります。 全量をmemoryに結合しません。producerの中でawaitでき、voidまたはRを返して終わります。
GDWebWriter | 動作 |
|---|
write(data, offset=0, count=-1) | byte列の範囲を送り、受け付けたbyte数を返す。送信が詰まっていれば進むまで待つ |
write_text(text, offset=0, count=-1) | 文字列の範囲をUTF-8で送る。offsetとcountの単位は文字、結果の単位はbyte |
flush() | それまでのwriteの送信完了を待つ。切断はここのエラーとreq.contextの取消でわかる |
- streamは一回限りです。応答ごとに新しく作ります。受信本文はstreamを返す前に読み終えてください。
lengthは送るbyte数です。宣言と実際が合わないと接続を閉じます。不明長(-1)はHTTP/2でDATA frame、HTTP/1.1でchunked、HTTP/1.0で接続終了が終端になります。
- HEADと本文を持てない状態番号ではproducerを呼びません。
- 長く待つproducerでは
req.contextの取消を確認してください。
- 1回のwriteが1つのchunkに対応するとは限りません。空の文字列や空のbyte列は終端になりません。
HTTPSとHTTP/2
HTTPSはapp.listen_tls(8443, "cert://chain.pem", "cert://key.pem", "127.0.0.1")で開始し、結果のRを確認します。 証明書のdirectoryは--mount cert=/path/to/certs:rで読取専用にします。PEMの鎖と暗号化されていない秘密鍵を渡します。 鍵の検証に失敗したときはportを開きません。
TLS 1.2と1.3に対応し、ALPNでHTTP/2とHTTP/1.1を選びます。HTTP/2の各streamは独立に進み、一つの取消は他のstreamを閉じません。 header_timeoutは未完了の握手にも適用されます。client証明書の要求は「TCPとUDP」のTLSの表を参照してください。
gzip圧縮
GD.data.gzip_writer(writer, level=-1)は、書いたbyte列をgzipにして下のwriterへ渡すGDGzipWriterを作ります。 下のwriterにはGDFileStream、TCP接続、GDWebWriterを使えます。全量をmemoryに貯めません。
| 項目 | 内容 |
|---|
| method | write(bytes)、flush()、close()、reset(writer)。どれもRを返す |
level | -2(Huffmanのみ)、-1(既定)、0..9 |
close() | gzipの末尾を完成する。下のwriterは閉じない |
reset(writer) | エラーを消し、同じlevelで使い回す |
header | name、comment(NULを含まないLatin-1)、extra(65535 byteまで)、mod_time(Unix秒)、os(既定255)。最初の書込みより前に設定する |
HTTPで返すときはGD.web.header(GD.web.stream(producer), "Content-Encoding", "gzip")を返し、producerの中で圧縮器を作って書き、close()の結果を返します。 Accept-Encodingの確認とVaryの設定は呼出側で行います。秘密情報と外部入力を一緒に圧縮せず、圧縮済みの本文や部分応答には使わないでください。
待受addressとport
IPv6のlocalhostだけで待ち受けるにはapp.listen(8080, "::1")!を指定します。strictでは--allow-net=[::1]:8080、接続先はhttp://[::1]:8080/です。 ::1と127.0.0.1は別の待受で、全interfaceを示す::とも異なります。
空きportをOSに選ばせる場合はapp.listen(0)の直後にapp.port()を読みます。待受けを保持したまま番号を得るので、他のprocessに取られません。 strictでは選ばれるportを事前に限定できないため、--allow-net=127.0.0.1のようにhost全体を許可します。 GD.net.free_port()とis_free()は診断用の瞬間的な確認で、その番号を確保する機能ではありません。
HTTP clientの接続
- HTTPSではHTTP/2を使い、同じ宛先への並行要求は一本の接続を共有します。非対応の相手と平文HTTPにはHTTP/1.1を使います。
- HTTP/1.1の接続は、本文を末尾まで読むと同じ宛先へ再利用します。空き接続は全体100本、宛先ごと2本、90秒まで保持します。
- 再利用した直後に閉じられた場合、安全に再送できるmethodだけ1度開き直します。
- HTTP/2では、相手が未処理と明示した要求だけを最大7回、間隔を延ばしながら再送します。要求の期限と取消は守ります。
Webの設定一覧
GD.http.fetch()とGD.webの各関数に辞書で渡す設定と、その既定値です。時間は秒、大きさはbyteです。
| 入口 | 設定と既定 | 意味 |
|---|
GD.http.fetch | method="GET", headers={}, body=null | HTTP method、送信header、送信body |
| 同上 | timeout=30.0, max_body=0 | 要求全体の秒と応答bodyのbyte。0は上限なし |
| 同上 | save="", sha256="" | 2xx bodyをsaveへ逐次保存し、返却bodyは空。sha256はsave必須の64桁hexで、一致した完了fileだけを置く |
| 同上 | authority="host:port" | CONNECTだけのrequest target |
GD.cli.run | timeout=0.0, output=true | 子processを諦める秒と、出力を集めるか |
GDWebApp.limits | jobs=0, job_timeout=0.0 | 保持する非同期handler数と秒。0は無制限 |
| 同上 | header_timeout=0.0, body_timeout=0.0 | request header/bodyを受け終える秒。0は無期限 |
| 同上 | header_bytes=1048576, header_values=2147483647 | request lineを含むheader byteと、header行数 |
GD.web.jwt_sign | ttl=900 | iat/expを補う秒。0は自動付与しない |
GD.web.jwt / jwt_verify | leeway=0.0, require_exp=true | 時刻許容秒とexp必須化 |
| 同上 | iss="", aud="", keep="jwt" | 空でない場合のissuer/audience一致と保持名 |
| 同上 | check=Callable() | 署名検証後にclaimを受け取る失効判定。指定時は真だけを許可 |
GD.web.sessions | total=1024, per_user=3 | process内の全session数と同一user数 |
| 同上 | idle=1800, life=43200 | 無操作と最大生存の秒 |
| 同上 | cookie="sid", keep="user" | Cookie名とrequest内の保持名。Cookie名はASCIIのtoken文字 |
GD.web.rate | limit=60, window=60.0 | keyごとの回数と固定窓の秒 |
| 同上 | keys=10000, key=Callable() | process内で保持するkey数とkey選択関数 |
| 同上 | trusted_proxies=PackedStringArray() | 転送元IPを信頼するproxyのIPまたはCIDR |
GD.web.csrf | allow_missing=false | 状態変更でFetch Metadataが無いclientを許すか |
GD.web.text_rule | min=0, max=4096 | textの文字数 |
GD.web.int_rule | min=-9223372036854775808, max=9223372036854775807 | 64 bit整数の範囲 |
GD.web.number_rule | min=-1e308, max=1e308 | 有限浮動小数の範囲 |
GD.web.list_rule | min=0, max=1024 | 要素数 |
GD.web.object_rule | extra=false | 未定義fieldを残すか |
GDWebApp.limitsは表にある6つの設定名だけを受け、綴り違いやbody_limitを誤りとして拒否します。
数値の設定が受け付ける範囲です。範囲外の値は設定時に失敗します。
| 設定 | 受理範囲 |
|---|
jobs | 0..2147483647。0は無制限 |
header_values, sessionのtotal/per_user, rateのlimit/keys | 1..2147483647 |
job_timeout, header_timeout, body_timeout | 有限の0..9223372036.854776秒。0は無期限 |
sessionのidle/life | 1..9223372036秒 |
header_bytes | 1..2147479551 byte。本文とは別 |
req.limit、GD.http.fetch.max_body | 0..9223372036854775807 byte。max_bodyの0は上限なし |
ttl | 0以上 |
leeway | 有限の0以上 |
database
GD.database.client()が返すclientは、SQLiteとPostgreSQLを同じ書き方で扱います。 local開発は組込みSQLite、本番はPostgreSQLという切り替えは、open()に渡すdriverで行います。
func main():
var local := GD.cli.env("DB_DRIVER", "sqlite") == "sqlite"
var db := GD.database.client()
db.open({
"driver": "sqlite" if local else "postgres",
"path": "user://app.sqlite3",
"host": "127.0.0.1",
"database": "app",
"user": "app",
"password": GD.cli.env("PGPASSWORD", ""),
})?
db.query("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")?
db.query("INSERT INTO users (id, name) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING", [1, "ada"])?
var out := db.query("SELECT id, name FROM users WHERE id=$1", [1])?
print(out.rows[0].name)
db.close()
return 0, null
表の作成もINSERTもSELECTもquery()一つで送ります。受け取るのはcolumns、rows、tagを持つ辞書で、 rowsは列名を鍵にした辞書の配列です。上の例ならout.rows[0].nameがadaになります。 SQLの値は$1、$2の順でbindし、両driverで同じ書き方です。SQLは変換しないため、両方で通るSQLを使います。
| method | 用途 |
|---|
query(sql, args) | 結果を全部集めて返す |
query_row(sql, args) | 先頭1行だけ返す。行が無ければErr.NOT_FOUND |
query_rows(sql, args) | GDDatabaseRowsを開き、1行ずつ読む。大量の結果向き |
stats() | 接続数、使用中、空き、待ち回数、待ち時間、接続を閉じた理由別の累積数 |
query_rows()はwhile rows.next()で進め、scan()で列名付きの辞書、values()で列順の配列を得ます。 next()がfalseになったらerr()を調べます。途中で止める場合はclose()を呼びます。
func list_users(db):
var rows := db.query_rows("SELECT id, name FROM users ORDER BY id")?
while rows.next():
var user := rows.scan()?
print(user.id, " ", user.name)
if rows.err() != null:
return R.err(rows.err())
return R.ok()
制約違反ではresult.e.infoに機械判定用の情報が入ります。violationはduplicate、not_null、foreign_keyのいずれか、 columnsは関係する列名です。PostgreSQLではcode、table、constraintもserverが返した場合に入ります。 値そのものはinfoへ残しません。SQLite自身が報告した失敗ではsource="sqlite"と拡張source_codeを保ちます。 SQLiteのforeign key文面には列名が無いため、その場合のcolumnsは空です。
func save(db):
var saved := db.query(
"INSERT INTO users(id,name) VALUES($1,$2)",
[1, "ada"])
if not saved.ok and saved.e.info.get("violation") == "duplicate":
var columns := saved.e.info.get("columns", PackedStringArray())
print("重複した列: ", columns)
transactionとmigration
複数の更新を一つの成否にするときはtransaction()を使います。callbackには同じ接続へ固定された GDDatabaseTxが渡ります。callbackが成功のRを返すとcommitし、失敗のRを返すとrollbackします。
func save(db, id, title):
return db.transaction(func(tx):
tx.query("INSERT INTO posts(id,title) VALUES($1,$2)", [id, title])?
tx.query("UPDATE counters SET value=value+1 WHERE name='posts'")?
return R.ok(id)
)
- callbackでは渡された
txを使い、必ずRを返してください。transaction中は元のclientのquery()と二重transactionを拒否します。
- commitの失敗はそのまま失敗として返ります。
- closeや取消はCOMMITの開始前ならrollbackし、開始後なら結果が確定してから接続を閉じます。
- callbackが終わった後は、保存しておいた
txも新しいSQLを受け付けません。
schemaを順番に適用するときは、SQLをsemicolonで分割せず、statementの配列をmigrate()へ渡します。 途中の一文が失敗すると全体をrollbackし、成功時は適用した文の数を返します。 versionとchecksumはapplication側で管理します。
func migrate(db):
return db.migrate([
"CREATE TABLE posts(id INTEGER PRIMARY KEY, title TEXT NOT NULL)",
"CREATE INDEX posts_title ON posts(title)",
])
databaseの高度な機能
driverの違い
| 項目 | SQLite | PostgreSQL |
|---|
| 向く用途 | local開発、単一process | 本番、異常終了耐性、複数worker |
| 接続 | clientごとに一つ。journalと一時表はmemoryに置く | 既定max(4, CPU数)までのpool。pool=25のように最大数を指定できる |
| 追加の入口 | 短い処理をその場で行うGD.database.sqlite.open() | まとめ送り、配列、JSONBを使うGD.database.postgres |
| 注意 | 既存の-journal、-wal、-shmがあるdatabaseは、通常のSQLiteで回復またはcheckpointしてから開く | loopback以外のhostではTLS証明書とhost名を既定で検証。loopbackはTLSなしが既定 |
GD.database.postgres.client()とGD.database.redis.client()のopen()はopen(host, port, opts)の形で、接続先を引数に取ります。
SQLiteの並行
同じclientのquery()は受付順に実行します。別のclientは並行に進みますが、同じdatabase fileへの書込みはSQLiteのlockに従います。 GD.database.sqlite.open()が返すGDSQLiteDBとGDSQLiteStatementは、呼出し元でそのまま実行する同期APIです。 短い処理だけに使い、同時利用はしないでください。並行処理にはGDDatabaseClientを使います。
PostgreSQLの接続と型
- poolは最初の問い合わせまで接続を作らず、需要の分だけ最大数まで増やします。上限に達した後の問い合わせは受付順に待ちます。
- 通常の
query()は使用中の接続へも続けて送ります(pipeline)。同じ接続では送った順に結果が返ります。
- transactionと
query_rows()は接続を一本専有します。接続固有の状態を使う処理は、BEGINを単発で送らずtransaction APIを使ってください。
- 同じ接続へ複数のSQLをまとめて送るときは
query_many、fetch_many、exec_manyを使います。
- 取消と期限超過は呼出し元へすぐ通知しますが、server上のSQL停止までは保証しません。他のqueryは中断しません。
stats()のwait_countは接続の取得待ちの回数で、pipeline内の応答待ちは含みません。
- 認証はSCRAM-SHA-256とMD5をserverの要求に合わせます。
auth="scram"またはauth="md5"で固定できます。MD5は旧server用です。平文passwordは明示した許可が要ります。
- JSON・JSONB列は「fileとdata」のJSONと同じ規則で読み、64-bit整数を保ちます。重複名など曖昧な値は元のJSON文字列を返します。
bool[]、int[]、bigint[]、text[]は要素の型、null、多次元構造を保ちます。下限を明示した配列は元の文字列を返します。
- 接続はUTF8を指定します。serverが別のclient encodingへの変更を通知した場合はエラーで接続を閉じます。変更のSQL自体は実行済みの場合があります。
Redisの接続
- TLSの選び方はPostgreSQLと同じです。
open()のtimeoutで接続と応答の期限を秒指定できます。
- poolの
open()は接続先を設定するだけで、通信は最初のquery()から始まります。
- 使用中の接続は返却まで専有し、空きが無ければ受付順に待ちます。待機の取消は他の呼出しへ影響せず、実行中の取消はその接続を閉じます。
size()は確立中を含む接続数、in_flight()は取得待ちを含む未完了数です。
databaseの設定一覧
open()に辞書で渡す設定と、その既定値です。
| 入口 | 設定と既定 | 意味 |
|---|
GDDatabaseClient.open | driver="postgres", path="" | driverとSQLite path。SQLite時はuser://...または:memory:が必要 |
| 同上 | host="127.0.0.1", port=5432 | PostgreSQLの接続先 |
| 同上 | pool=0 | PostgreSQL最大接続数。0はmax(4, CPU数)、SQLiteでは使わない |
| 同上 | max_rows=0, max_bytes=0 | query()が集める1結果の行数とbyte。0は無制限。query_rows()には適用しない |
GDPostgresClient.open | user="postgres", database="postgres", password="" | 認証とDB名 |
| 同上 | connect_timeout=15.0, timeout=0.0 | 接続と問い合わせの秒。poolの接続待ちも問い合わせ時間に含む。0は無期限 |
| 同上 | auth="any", allow_cleartext_password=false | auth="scram"/"md5"で方式固定。平文password応答は明示時のみ |
| 同上 | tls=<hostで決定>, ca="" | 外部hostはverify-full、loopbackはdisable。CA fileは明示時だけ |
GD.database.sqlite.open | busy_ms=5000, max_ms=0 | lock待ちミリ秒と実行期限ミリ秒。0は無期限 |
| 同上 | max_rows=0, max_bytes=0 | 1結果の行数とbyte。0は無制限 |
GDRedisClient.open | password="", timeout=10.0 | passwordと接続・応答期限の秒。0は無期限 |
| 同上 | tls=<hostで決定>, ca="" | PostgreSQLと同じTLS選択 |
GD.database.postgres.pool | size既定0、0または1..2147483647 | 0はmax(4, CPU数) |
GD.database.redis.pool | size既定0、0..2147483647 | 最大接続数。0は無制限。同時に作る接続はCPU数の10倍まで、最大数の指定時はその数まで |
GDRedisPool.open | pool_timeout=timeout+1.0(timeoutが0なら30秒) | 接続の空きを待つ期限。明示0は無期限 |
max_rowsまたはmax_bytesを越えたquery()は、その問い合わせだけを失敗にします。 期限切れや壊れた応答で順序を失った場合は接続全体を閉じます。
| 設定 | 受理範囲 |
|---|
max_rows, max_bytes, busy_ms, max_ms | 0..2147483647 |
| bind値 | PostgreSQLは65535個、SQLiteはengineの変数上限まで。query_manyの件数に固定上限はない |
| PostgreSQLの1送信 | SQLとbind文字列をUTF-8のbyteで数え、約1 GiBまで |
| Redisの1送信 | server側の設定に従う |
| PostgreSQLとRedisのport | 1..65535 |
| 秒指定 | 有限の0..9223372036.854776秒。0は無期限 |
定期処理
決まった時刻に一度だけ動かす仕事は、普通のscriptとして書き、OSのcronやsystemd timerから呼びます。 gd側に常駐の仕組みは要りません。
func collect():
var now := GD.time.to_iso(GD.time.now())
GD.file.append_text("store://log.txt", now + "\n")?
return 0, null
func main():
collect()?
return 0, null
gd --strict --mount store=/var/lib/app:rw collect.gd
自分で間隔を持って回り続ける仕事は、GD.async.spawn()へ渡してgd serveで常駐させます。 spawn()へ渡した処理はmain()が返った後も動き続けます。
func every(sec, fn):
while true:
await GD.async.sleep(sec)
fn.call()
func collect():
print(GD.time.to_iso(GD.time.now()))
func main():
var _job := GD.async.spawn(every.bind(60.0, collect))
return 0
gd serve schedule.gd
止めるときはprocessを終わらせます。Web serverと同じ常駐なので、ここでもserveが必要です。
公式拡張モジュール
本体を小さく保ち、外部service固有の機能は必要なprojectだけへGDScript packageまたはGDExtensionとして加えます。
| 入口 | 用途 | APIと導入方法 |
|---|
Discord | DiscordのGatewayとRESTを使う純GDScript文字Bot | Discord Bot |
GDMemcached | TCP接続を再利用するcache client | Memcached |
GDSupabase | DatabaseとAuthのclient | Supabase |
各文書に公開class、method、戻り値、制限値、strict実行例をまとめています。任意導入のため、 本体だけから生成するAPIリファレンスには含まれません。
gd addで入れた拡張は起動時に信頼して読み込むため、旗は要りません。接続先の--allow-netは必要です。
--allow-extと--deny-extが効くのは、scriptが実行中にGDExtensionManager.load_extension()で読む場合です。
- 入れた拡張はprocessと同じ権限で動くので、信頼する版を
gd.lockで固定してcommitしてください。
packageと配布
scriptが増えたり他のpackageを使ったりする段階で、gd initでgd.jsonを作ります。依存はgd.jsonとgd.lockで固定します。
gd init
gd search discord bot
gd add gd:@scope/script-package@^1.0.0
gd add ext:@scope/name@^1.0.0
gd add short-name https://example.com/module.gd
gd install --frozen
gd task test
packageを使う
入れたpackageは、利用側が決めた呼び名を使ってpkg://<呼び名>/から読みます。
const Hello := preload("pkg://hello/mod.gd")
pkg://は利用者ごとの共有cacheを指し、projectへは何も複製しません。
gd.jsonに書いた依存がcacheに無ければ、最初の実行で取得します。--strictでは登録所への--allow-netが要ります。
gd addの既定の呼び名は、package名の-と.を_にした識別子です。engine classやkeywordと同じ呼び名は断ります。
- commitするのは
gd.jsonとgd.lockです。gd initはpkg/を.gitignoreへ書きます。
--frozenはlockを変更しません。offlineの配布先では、networkのある環境で先に取得し、--cached-onlyを併用します。
- install、add、updateが途中で失敗したときは、projectの配置とlockを元へ戻します。
- lockは登録所に結び付いています。別の登録所へ切り替えるには明示的なlock移行が必要です。
importの短い書き方
@importはconst 名 = preload(...)の短い書き方です。
@import greet # 呼び名 → pkg://greet/mod.gd、識別子は greet
@import greet/style as Style # 呼び名の中のscript
@import "./util.gd" as Util # 相対fileを明示
@import "./net/client.gd" # 下位directoryのfile
@import "../shared/util.gd" # . や scheme で始まるpathは引用符で書く
@import "./net/mod.gd" as n
- 引用符の無い名前は、
gd.jsonのimportsに宣言した呼び名だけを解決します。同名のfileを探しに行きません。
- 相対fileは引用符で
./または../から書きます。
- 識別子は
asが無ければ最後の要素そのままで、mod.gdを持つdirectoryはdirectory名です。
gd fmtは@importをそのまま残します。
- 本家Godotは
@importを知らないので、Godotと共有するfileではconstとpreloadを書いてください。
packageを作る
packageはgd.jsonを根に持つ一つのprojectです。gd init @scope/nameがmod.gdとtestの雛形を作り、 gd testで回し、gd publishで公開します。
{"name":"@scope/hello","version":"1.0.0","main":"src/mod.gd","include":["src"]}
gd publish
gd add hello gd:@scope/hello@^1.0.0
- 入口は
mod.gdです。複数fileならincludeへfileまたはdirectoryを明示します。
- mainのdirectoryがpackageの根になるので、package内の相対preloadはそのまま動きます。
- packageは自分の
gd.jsonのimportsで他の登録所packageを使えます。gd publishがそのimportsを登録所へ載せます。
class_nameは公開できます。installは同名classの衝突を検査し、衝突すれば全体を元へ戻します。
gd.jsonのgodotをtrueにすると、gd固有のAPIを使わず本家Godotでも動くという作者の宣言になり、gd searchが[godot]と示します。
開発中のpackageはgd add ../pathでlocalから足します。呼び名は先のgd.jsonのnameから取ります。 checkoutをpkg/<呼び名>/へ複製し、内容の指紋が変われば次の実行で複製し直します。 .で始まるfile、pkg/、tmp/、gd.jsonを持つ下位directory、tokenは複製しません。 gd publishは、local importの先にnameとversionのあるgd.jsonがあれば登録所の範囲に変換し、無ければ拒みます。
依存の解決
gd installは依存graph全体を解決します。版は、gd.lockが固定した版、今回すでに選んだ版のうち範囲を満たすもの、 登録所の最新一致の順で選びます。
gd.lockは解決したimportsの設定も保持します。設定が変わった実行では同じresolverで再解決し、要求外の古い版を使いません。 --frozenは設定の不一致を拒否します。同じpackageに複数の別名がある場合、辞書順で最初の別名を配置先に使います。
- 純GDScript packageは、版ごとに別のものとして共存できます。
- native拡張はprocessに一つしか読めないため、一つの版に揃えます。範囲が両立しなければ取得前に止まります。
- 同じhost instanceを共有するpluginの仕組み(peer依存)はありません。
- 登録所packageの正式なpathは
pkg://@scope/name@版/です。pkg://<呼び名>/は、書いたscriptが属するpackageのimportsで正式pathへ展開されます。同じ呼び名でもpackageごとに違う版を指せ、同じ版はどこから辿っても一つのscriptです。
gd.lockには各packageのimportsの解決先も記録され、gd infoが一覧します。
gd removeとgd updateは、どのpackageも使わなくなったものをgd.lockとpkg/から外します。
- 検索の順位が変わっても、既知のpackageのinstallとlockの検証には影響しません。
Godotと共有する置き場
本家Godotなどres://しか読めない環境と共有するときは、gd.jsonへ"place": "project"を書きます。 packageをpkg/<呼び名>/へ複製し、pkg://もres://pkg/もそこを指します。 他のpackageだけが使うものはpkg/@scope/name@版/へ置きます。
project.godotのあるdirectoryではplaceの既定がprojectになり、.gitignoreは書きません。gdの無い同僚が開けるようpkg/をcommitします。
- installは
preload、load、extendsに書かれたres://参照を配置先へ書き換えます。文字列、コメント、実行時に組み立てるpathは書き換えません。
placeはfileの置き場を決めるだけで、gd固有のAPIや構文をGodot向けに変換する機能ではありません。共有するsourceは標準構文と相対preloadで書きます。
native拡張のpackage
- native拡張は読込みに実fileが要るため、
placeに関わらずpkg/<呼び名>/へ置きます。
- scriptから名指せるのは、自分のpackageが
ext:で取り込んだ拡張のclassだけです。projectのscriptならgd.json、packageのscriptならそのpackageのimportsが基準です。
- 登録所の外にある拡張はprojectのscriptだけが使えます。
- 登録所のpackageの拡張が、manifestの
[classes]に無いclassを登録すると起動時に止まります。
- 配布先のOSで取得するか、
gd compileを配布先のOSで実行してください。
設定と環境変数
gd.jsonの設定は次の10件です。
| 名前 | gd initの生成値 / 未指定時 | 意味 |
|---|
name | my-tool / 必須 | project名。publishは@scope/nameが必要 |
version | 0.1.0 / 必須 | packageのversion |
tasks | run/testの2件 / 無し | gd taskから呼ぶcommand |
imports | {} / {} | 呼び名と依存先。publishするpackageでは登録所packageだけ |
registry | 未指定 / 環境または公開登録所 | project固定の登録所URL |
main | 未指定 / mod.gd | publishするmod.gdまたは.gdextension入口 |
include | 未指定 / mainだけ | 純GDScript packageへ含めるmain directory内のfileまたはdirectory |
place | 未指定 / cache(project.godotがあればproject) | packageの置き場。projectでpkg/へ複製する |
godot | 未指定 / false | gd固有のAPIを使わず本家Godotでも動くpackageの宣言 |
description | 未指定 / 空 | 登録所に出す説明 |
gdが読む環境変数は次の通りです。scriptから環境を読む実行では--allow-envで名前を許可します。
| 環境変数 | 用途 |
|---|
GD_CACHE_HOME | packageのcache根。未指定はWindowsのLocalAppData内gd。macOS/Linuxは絶対pathのXDG_CACHE_HOME/gd、それがなければhome内.gd。HOME未設定時はOSの利用者情報を使う |
GD_REGISTRY | 登録所。未指定はhttps://gd-cli.progsha.com/pkg。gd.jsonのregistryが優先 |
GD_TOKEN | publishのtoken。設定fileへ書かず、publishするprocessだけへ渡す |
LC_ALL、LANG | gd docの手引きの言語 |
GD_REGISTRY_HOST | 登録所の待受address。既定127.0.0.1。コンテナ内では0.0.0.0を指定 |
GD_REGISTRY_DATA | 登録所の保存先。既定data。strict起動では書込可能なmountを指定 |
PORT | 同梱の登録所tools/registry.gdの待受port。既定8787、1..65535 |
GD_WORKER | --workersが作る内部印。利用者が設定する値ではない |
遠隔packageと登録所はHTTPSを使います。loopbackの開発用登録所に限りHTTPも使えます。 取得したpackageとnative libraryは登録所索引のSHA-256と照合します。 .gdextension manifestは16 MiB、packageの全file合計は500 MiBまでです。
単一実行体で配布する
compileで、script、view、静的file、migration、依存package、対象OSのGDExtensionを一つの実行体へまとめます。配布先にcacheは要りません。
gd compile -o app main.gd
./app
gd.jsonが名指すpackageと、それらが取り込むpackageを全部同梱します。
- 同梱したWebアプリも
./app serve --no-scene-tree --allow-netで常駐できます。
- local pathのpackageからは、
.envなど.で始まるfileとgd.jsonのtokenを除きます。
- secretをsourceへ埋め込まないでください。compileは
.envを除外しますが、sourceに書いた値は実行体へ残ります。
対応範囲と報告
gdはAPIが固まる前の公開版です。後方互換は前提にしないでください。変更した点と基準にしたGodotの版は CHANGELOGに書きます。 gdはGodot FoundationまたはGodot Engine projectの公式製品ではありません。
不具合はIssuesへ、公開すべきでない脆弱性は GitHubの非公開報告から知らせてください。
gd Manual
The online manual and API reference uses your browser’s preferred English or Japanese language, with English as the fallback. Use the language button or ?lang=ja for Japanese; an explicit selection is remembered locally.
What gd is for
gd is a single command for writing command-line tools, websites, Web APIs, scheduled jobs, and data processing in GDScript. It is Godot built small without a display, and it runs a single .gd file without a project.godot.
You start the way you would with a Python or Node.js script. When you need them, type checking, tests, packages, databases, a Web server, and a standalone executable are available in the same GDScript. For game screens and rendering, use upstream Godot.
Together with Godot itself, apps, frontends, servers, and CLI tools can all be written in one language, GDScript. The same script runs on macOS, Linux, and Windows, and waits on networking and databases do not stop other work. It links directly with C++ through GDExtension. It is designed for AI agents to write and run code.
Install
Supported platforms are macOS arm64/x86_64, Linux x86_64, and Windows x86_64. Download the archive for your OS from Releases and put the gd inside on your PATH. When gd --version prints a version, the install is done. The SHA-256 of each archive can be checked against the bundled SHA256SUMS. The macOS build is signed with a Developer ID and notarized by Apple.
Building from source needs Python, uv, SCons, and a C/C++ compiler. The executable is gd.*.template_release.* under bin/. TLS is built in, so no separate TLS library is needed.
git clone https://github.com/prog-sha/gd-cli.git
cd gd-cli
scons platform=macos target=template_release -j8
# Linux: platform=linuxbsd
# Windows: platform=windows windows_subsystem=console
Quick start
Create one file, hello.gd. No config file or package is needed. main() is the entry point, and the integer it returns becomes the process exit code.
func main():
print("Hello, world")
return 0
gd hello.gd
To check types and syntax without running, use check.
gd check hello.gd
Pick by purpose
The standard API has one entry point, GD, with a child per purpose. Scripts write these names as they are.
| What you want | Entry | Example |
|---|
| Files, text, time, HTTP client, async | GD | GD.file.read_text("a.txt") |
| Websites and Web APIs | GD.web | GD.web.app() |
| SQLite or PostgreSQL | GD.database | GD.database.client() |
GD.database.postgres and GD.database.redis are advanced entries for features specific to one backend.
Looking up the API
Pass the same spelling you write in a script to gd doc. Signatures come from the executable, so they match the implementation.
gd doc # short guide and entry points
gd doc manual # the full manual
gd doc GD # the children of GD
gd doc GD.file # file API
gd doc GD.http.fetch # the returned HTTP response
gd doc GD.web.app # Web application
gd doc SceneTree # Inspect a public engine class.
gd doc all # every public class
Return types such as R, Err, and GDWebRequest are looked up by name alone. For Godot classes such as Node, SceneTree, and Timer, see the Godot class reference as well. The manual is shown in Japanese when LC_ALL or LANG starts with ja, and in English otherwise. The web version at gd-cli.progsha.com switches between Japanese and English.
GDScript basics
The examples avoid repeating type names. Variables assigned with := and success values returned as return value, failure infer their types. Parameters without annotations remain dynamic. Add annotations only where you want to fix a type boundary.
Arguments and flags
Arguments placed after the script name arrive in main(argv). An argument that could be mistaken for gd's own flag, such as --name=gd, reaches the script when placed after --.
func main(argv):
for arg in argv:
print(arg)
return 0
gd main.gd apple orange
gd main.gd -- --name=gd
To interpret them as flags, use GD.cli.flags(). It accepts --name gd, --name=gd, and -name=gd alike.
func main(argv):
var flags := GD.cli.flags()
flags.flag_str("name", "world", "who to greet")
var parsed := flags.parse(argv)
if not parsed.ok:
print(flags.usage())
return 1
print("Hello, " + flags.get_str("name"))
return 0
Calling external commands
Call external tools with GD.cli.run(). Only the calling GDScript waits, so other requests proceed even when it is called from a gd serve handler. Under --strict it needs --allow-run. You can narrow the target, as in --allow-run=/usr/bin/git.
func main():
var got := GD.cli.run("git", ["rev-parse", "HEAD"])
if not got.ok:
return 1
print("code=", got.v["code"], " out=", got.v["output"])
return 0
The third argument, opts, changes the behavior.
| Name | Default | Meaning |
|---|
timeout | 0 | Seconds before giving up. 0 is unlimited. Past it, the child is shut down and Err.TIMED_OUT is returned |
output | true | Collect the output. With false, the child uses the parent's standard I/O directly and nothing is collected |
Commands during development
gd check main.gd # check types and syntax without running
gd fmt main.gd # normalize formatting
gd test # collect and run *_test.gd
gd --watch main.gd # run again on every save
gd eval 'print(1 + 1)' # try one line
gd repl # try interactively
Values and failures
A function that can fail returns two values, the success value and the failure, instead of throwing. The caller receives both with var value, e :=, and a non-null e means failure.
func main():
var text, e := GD.file.read_text("note.txt")
if e:
print(e.text())
return 1
print(text)
return 0
Shorter failure handling
Instead of writing if e every time, append ? to the call. The failure is returned to the caller as is, and only the success value remains. A function that uses ? also returns a success value and a failure itself, with return value, failure.
func title(path):
var text := GD.file.read_text(path)?
return text.strip_edges(), null
func main():
var text, e := title("note.txt")
if e:
print(e.note("read the title").text())
return 1
print(text)
return 0
| Form | Meaning |
|---|
var value, e := call() | Receive the success value and the failure separately |
return value, null / return null, failure | Return success or failure. The success type is inferred from value |
call()? | On failure, return it to the caller as is |
call()! | On failure, print the reason and stop the program there with exit code 1. For prototypes and tests. Under gd serve, only that handler fails |
e.note("purpose") | Add working context to a failure. It prints as "purpose: original reason" |
e.kind | The kind, such as Err.NOT_FOUND or Err.INVALID_DATA. Use it to branch |
Err.err("reason", Err.NOT_FOUND) | Create a failure yourself |
In main(), which does not pass failures up, receive them with var value, e := or !.
File-operation failures put op, path, source, and source_code in e.info. A rename has old and new instead of path. source is one of posix, win32, and engine. A kind is set only when its meaning, such as NotFound, is known; an unknown I/O failure keeps its source details with Err.NONE.
Return value rules
Code works without written types. When you do write types, these are the detailed rules.
- A result signature has two types, such as
-> int, Err: one success type and Err. The runtime type is R; -> R or no return annotation is also allowed.
- A comma return always has two values. The last must have type
Err or be null for success. A string cannot be returned there directly; wrap it with Err.err(reason).
- Fix a failure variable's type with
var e: Err = ... or var e := Err.err(...). A variable declared with var e = ... can change type and cannot occupy the last slot.
- To return several items, put them in one array or dictionary, as in
return [1, 0.0, ""], null. return null, null returns null as the success value.
- Unpack an
R with exactly two names: var value, e := call(). A declaration that lists expressions, such as var a, b, c := 1, "a", 0.0, is a different thing: it has no count limit and infers each name from its expression. It may also assign variables visible in the function, including those of enclosing blocks, when it introduces at least one new name, and every right-hand expression is evaluated before any assignment. Constants, parameters, and variables a lambda captured cannot be assigned.
- Using
? in a function whose annotated return type lacks , Err gives The "?" operator needs a function returning "R" or "Err".
- Writing
return R.ok("a") under -> int, Err is a compile error. When the type is dynamic, it is checked at runtime.
- When a typed Array or Dictionary is the success value, give the original container the same element type.
- A function returning a value and a failure returns on every path. One that only propagates with
? still ends with return null, null.
- A function declaring a single return type such as
-> int cannot use a comma return. Declare -> int, Err.
- A lambda cannot use a comma return. Use
return R.ok(value) and return R.err(reason).
Carrying results in R
To carry the value and the failure around as one value, use R. Read ok for the outcome, v for the success value, and e for the failure. Build one with R.ok(value) or R.err(reason, kind, partial_value). Web handlers and database transactions can also be written to return this R.
func find(items, want):
for item in items:
if item == want:
return R.ok(item)
return R.err("not found: " + want, Err.NOT_FOUND)
func main():
var got = find(["a", "b"], "c")
if not got.ok:
print(got.e.text())
return 1
print(got.v)
return 0
R.ok() carries null as its success value, not integer 0. I/O APIs that make partial progress keep the completed amount in v as a partial value even on failure. note() preserves the partial value, and v_or(fallback) returns the fallback on failure. When ? propagates a failure whose partial value does not fit the caller's success type, only the partial value is dropped; the reason and kind are kept.
Waiting and concurrency
Waiting methods in HTTP, databases, GD.net, files, and others are written as ordinary function calls. Only the calling GDScript waits; other networking and timers keep running.
func main():
var res := GD.http.fetch("https://example.com/")
print(res.status)
return 0
To start several operations together, use the variants ending in _async with GD.async.all().
func main():
var got = await GD.async.all([
GD.http.fetch_async.bind("https://example.com/a"),
GD.http.fetch_async.bind("https://example.com/b"),
])
for res in got:
print(res.status)
return 0
| Entry | Purpose |
|---|
name_async() | Start the operation and return a Signal. await gives the same result as the regular name |
GD.async.all(list) | Accept Callables and Signals and return all results in input order. An invalid input becomes an error in its result slot |
GD.async.spawn(fn) | Run a GDScript function in the background. It keeps running after main() returns |
GD.async.sleep(sec) | Wait the given number of seconds |
Pass Callables rather than Signals to all(). It subscribes to completion before starting each one, so it cannot lose a result that finishes early.
A signal saved with := retains its completion type. Reassignment from a different or unknown completion type is rejected. For dynamic completions, declare the receiver as Signal and annotate the awaited value as needed, for example var result: R = await pending.
spawn() does not move CPU work to another thread. Long GDScript yields to other work automatically, but the inside of a native method is not interrupted, so use the _async variant when passing large input to a standard module. A waiting method may be called only from a function that GDScript itself called. Inside a function that native code calls back, such as an Array.map() callback, _init(), a member initializer, or _to_string(), it is an error; there, await the _async form or call it from a coroutine.
Tutorial: a notes API on SQLite
With what you have read so far, this builds a small API that accepts JSON and stores it in SQLite, in one script. The result is a development server with input validation and SQL parameter binding, started with narrowed permissions.
1. Create a working directory
mkdir notes-api
cd notes-api
2. Write the API
Save the following as main.gd.
# Store notes in an embedded database and expose a JSON API.
extends RefCounted
const PORT := 18080 # development port listening on loopback
const DB_PATH := "user://notes.sqlite3" # per-user writable area provided by gd
var app := GD.web.app()
var db := GD.database.client()
# Return notes as JSON, newest first.
func list_notes(_req):
var got := db.query("SELECT id, title FROM notes ORDER BY id DESC")?
return GD.web.json(got.rows), null
# Store a validated title and return the created row.
func add_note(req):
var body := req.valid("body")
var made := db.query(
"INSERT INTO notes(title) VALUES($1) RETURNING id, title",
[body.title]
)?
return GD.web.json(made.rows[0], 201), null
# Prepare the database and routes, then listen on loopback.
func main():
db.open({"driver": "sqlite", "path": DB_PATH})?
db.query("CREATE TABLE IF NOT EXISTS notes(id INTEGER PRIMARY KEY, title TEXT NOT NULL)")?
app.route("GET", "/notes", list_notes)
app.route("POST", "/notes", add_note, [GD.web.json_body(GD.web.object_rule({
"title": GD.web.text_rule({"min": 1, "max": 120}),
}))])
app.listen(PORT, "127.0.0.1")?
print("listening on http://127.0.0.1:%d" % PORT)
return 0, null
Read it from the top.
app is the router and db is the database connection. Both are script variables so the server can keep running after main() returns.
main() first opens SQLite and creates the table. user:// in DB_PATH is a per-user writable area provided by gd.
app.route() registers an HTTP method, a path, and the function to call for it (the handler).
- A handler receives a
GDWebRequest and builds the reply with GD.web.json(). A ? in the middle returns the failure to the server, which becomes a status such as 500.
- The POST route carries
GD.web.json_body(). The handler is called only when the body matches the rule, and the value that passed arrives in req.valid("body").
- SQL values are bound to
$1. SQL is never built by string concatenation.
3. Start with narrowed permissions
Run unverified scripts and servers exposed to the outside with --strict, which denies permissions by default. Here the listener is limited to one loopback port. serve keeps the process alive after main() returns, and it is the command to use for servers.
gd check main.gd
gd --strict --allow-net=127.0.0.1:18080 serve main.gd
4. Use it from another terminal
curl -s -X POST http://127.0.0.1:18080/notes \
-H 'Content-Type: application/json' \
-d '{"title":"try gd"}'
curl -s http://127.0.0.1:18080/notes
The first call returns status 201 with the created row, the second the stored array. An empty title, a title over 120 characters, or a non-JSON body is rejected with 400. Press Ctrl-C in the terminal that started it to stop.
In production, keep this process on loopback behind a TLS reverse proxy, and switch storage that must survive crashes to PostgreSQL. Keep connection details out of the source and read them from allowed environment variables.
Permissions
gd has two ways to run.
| Mode | Suited to | Restrictions |
|---|
| Normal execution | Running trusted source during development | Neither files nor the network are restricted |
--strict | Unverified scripts, public servers | res:// and absolute paths are read-only. Network, environment variables, child processes, native extensions, and system information are denied by default |
Under --strict, start by listing what the script uses.
gd --strict \
--mount store=/srv/app:rw \
--allow-net=db.example.com:5432 \
--allow-env=DATABASE_URL \
main.gd
| Flag | Grants |
|---|
--mount name=path:r / --mount name=path:rw | Read or read/write on a named directory |
--allow-net=host:port,... | Connecting and listening. Without a value, everything |
--allow-env=name,... | Environment variables |
--allow-run=command,... | Child processes |
--allow-ext=path,... | Native extensions a script loads while running |
--allow-sys=item,... | Machine and system information |
--deny-* | A denial that wins over the matching allow |
-A | Allow everything except files. For temporary use during development |
File locations
A script sees files through four kinds of location. Write the name of the location at the start of the path, or write an absolute path as it is.
| Spelling | Location | Under strict |
|---|
res://a.txt | The directory the script was started from | Read-only |
user://a.txt | Per-user writable area provided by gd | Read/write |
store://a.txt | The name given by --mount store=/srv/app:rw | As specified |
/etc/hosts | That location on the machine | Read-only |
- A relative path that climbs above
res:// is refused in either mode.
--mount and absolute paths are for Linux and macOS. Windows rejects them, so put files under res:// or user:// there.
- A mount name uses lowercase letters, digits, and
-. res, user, uid, pipe, local, libgodot, tcp, unix, http, https, file, data, and cache are reserved and cannot be chosen.
Network and extension permissions
- In
--allow-net, localhost:8080 also covers IPv4 loopback 127.0.0.0/8 and IPv6 ::1 on the same port.
*.example.com:443 allows its subdomains.
- Native extensions run in the same process, so allow only ones you trust.
serve and SceneTree
gd serve is the resident way to run, and it creates no SceneTree. Networking, timers, await, custom Signals, GD.async.sleep(), and queue_free() on nodes outside a tree all work. With no work to do, it sleeps until the next deadline or network notification, so there is no cycle to tune.
| What you want | How |
|---|
| Keep a Web server or scheduled job resident | gd serve main.gd |
Use Node _process(), _physics_process(), process_frame, SceneTreeTimer, or high-level multiplayer | Normal execution without serve |
| Run a script extending SceneTree or MainLoop | Normal execution without serve |
| Check during development that no SceneTree slips in | gd --no-scene-tree --allow-net serve app.gd |
| Listen with several processes | --workers=<n> or --workers=auto. n is an integer of 1 or more |
Under serve, a script that only extends Node is not added to a tree. --no-scene-tree reports a diagnostic as soon as a SceneTree is created and exits with code 1. It is inherited by --watch and --workers children. Normal execution creates an implicit SceneTree, so it fails with this flag.
TCP and UDP
Use GD.net for low-level networking. Godot's low-level types remain for compatibility, but new code should use GD.net.
func echo():
var listener := GD.net.listen_tcp("127.0.0.1", 8080)?
var conn := listener.accept()?
var data := conn.read(65536)?
conn.write(data)?
conn.close()
return 0, null
GDTCPConn keeps reads and writes in separate queues, so several GDScripts may call it concurrently.
- Deadline methods set durations from now; zero clears them.
close() releases pending reads and writes with Err.INTERRUPTED. Listener accepts behave the same way.
- A connection tries the IPv4 and IPv6 candidates from name resolution in order and keeps only the one that succeeds. The overall
timeout is never extended.
TLS
Open TLS with GD.net.dial_tls(host, port, opts). It verifies the certificate chain and host name by default, and a failure never falls back to plaintext. The result is the same GDTCPConn used for TCP.
opts | Meaning |
|---|
timeout | One deadline in seconds covering both connect and handshake |
ca_file | A private CA. It takes precedence over the environment settings |
cert_file, key_file | Client authentication. Provide both, as files inside permitted mounts |
server_name | Check the certificate against a name different from the dial address |
next_protos | An array of ALPN names. One name is 1–255 bytes, and the whole list is up to 65535 bytes |
insecure_skip_verify | Skip verification. Use only for tests where verification is deliberately unnecessary |
Read the negotiated result from negotiated_protocol and version in connection_state(). TLS 1.2 is 771 and TLS 1.3 is 772.
Without other settings, the trusted CAs are the OS trust settings on macOS and Windows, and the system CA bundle on Linux. Set SSL_CERT_FILE or SSL_CERT_DIR before starting the process to use the given CAs on any OS. Directory lists use : on Unix and ; on Windows.
When a server requires client certificates, pass client_ca (a trusted CA bundle) and client_auth in the opts of app.listen_tls(port, cert, key, host, opts). A missing client_ca uses the system trust settings.
client_auth | Behavior |
|---|
none | Does not request a certificate |
request | A certificate is optional. It is not verified |
require | A certificate must be presented. It is not verified |
verify_if_given | Verifies a certificate only when one is presented |
require_and_verify | Requires a verified certificate |
UDP and name resolution
GD.net.listen_udp() returns a GDUDPPacketConn. read_from() returns a dictionary containing data, host, port, and truncated. Pass an IP address resolved by GD.net.resolve() as the host of write_to(). Packets are never merged. The default buffer=0 keeps the OS receive buffer as it is; only a positive value requests a change.
GD.net.resolve() returns the first address selected by the OS. It keeps no name cache. GD.net.local_addresses() returns the machine's address list and distinguishes an empty list from an OS failure. The failure's e.info carries syscall, source, and source_code.
Files and data
GD.file reads and writes files and handles paths. The directory you started from is res://, and absolute paths work as written. To write to an outside directory under strict, write the name given by --mount store=/srv/app:rw as in store://users.csv.
func main():
var rows := GD.data.csv_objects(GD.file.read_text("store://users.csv")?)?
GD.file.write_text("store://users.json", JSON.stringify(rows))?
return 0, null
File operations suspend only the calling GDScript, even under their regular names. Other requests proceed while a Web server handler reads a file. Use the variants ending in _async only to start several operations together.
func handler(_req):
var body := GD.file.read_text("store://big.json")
if not body.ok:
return GD.web.text("cannot read", 500)
return GD.web.text(body.v)
Files embedded by compile can be read, listed, and served statically through the same API.
Reading large files
To read without holding the whole file in memory, open a GDFileStream with GD.file.open(path, mode). Modes are read, write, append, and read_write. Call close() when done.
| Method | Behavior |
|---|
read(max) | Returns up to max bytes. It may return fewer. An empty successful value is EOF |
write(bytes) | Writes all bytes and returns the count. On a failure partway, R.v retains the number already written |
Operations on one stream run in arrival order, and separate streams proceed in parallel. Append always writes at the end, even after a seek. read_bytes() also retains the bytes already read in R.v when it fails partway. read_text() rejects input too large for a String instead of truncating it, so handle large files as bytes or a stream.
Files updated concurrently
When several processes update the same file, use GD.file.replace_text(path, old, body). It replaces the content only when the old you read still matches the current content, so a concurrent edit is never silently overwritten. Pass null as old to create a new file.
Entry points by data format
| Purpose | Entry |
|---|
Reading CSV, TOML, YAML, JSONL, JSONC, XML, INI, TAR, front matter, and .env files | GD.file.read_csv(path) and similar |
| In-memory conversion of the same formats, JSON, codecs, hashes, HMAC, PBKDF2, HKDF, byte sequences | GD.data |
| UUID and ULID | GD.id |
| Time conversion and arithmetic | GD.time |
| Text formatting and comparison | GD.text |
| HTML entities, tags, and gdhtml (a micro template with Mustache syntax) | GD.html |
| Flags and environment variables | GD.cli |
| Array and dictionary operations | GD.collection |
| Special math values and bit operations | GD.math |
| Version comparison | GD.version |
| Logging to the terminal and files | GD.log |
| Test assertions | GD.test |
Environment variables and .env have separate entries by what you read.
| What you read | Entry |
|---|
| Process environment variables | GD.cli.env(name, fallback) and GD.cli.require_env(name). Strict mode needs --allow-env |
A .env file | GD.file.read_env(path). Reads the file into a dictionary |
| A dotenv string | GD.data.env(src) and GD.data.to_env(data). Convert to and from a dictionary in memory |
In-memory conversions compute in place under their regular names, and their _async variants compute on another thread. Use _async for large inputs. Run gd doc GD.file and gd doc GD.data for the exact lists. Checks and limits per format are in the description of each entry in the API reference.
GD.collection operations that take a Callable yield to other work about every 1 ms. Each GD.log call waits until the write completes and never truncates the message. Check failures through the returned R; GD.log.flush() waits for all earlier output.
JSON rules
Use GD.data.json_encode(value) to produce JSON bytes and GD.data.json_decode(bytes) to read bytes received from outside. Both return a success value and an Err. GDWebRequest.json(), GDHTTPResponse.json(), and each JSONL line follow the same rules.
- Invalid UTF-8, duplicate names, non-finite numbers, unsupported types, and cycles fail instead of being turned into ambiguous values.
- An integer within the signed 64-bit range returns as
int; only fractions, exponents, and out-of-range values become float. Strings and keys preserve \u0000.
- Pass
{"deterministic": true} when the same value must give the same bytes, as for signatures or cache keys.
- The options
deterministic and escape_html are bool; max_bytes and max_depth are int. An invalid type returns Err.INVALID_DATA, and exceeding a limit returns Err.LIMITED.
- Keep the input Arrays, Dictionaries, and their children unchanged until
json_encode_async() finishes. The options dictionary is copied at the start.
Hashes and key derivation
GD.data returns SHA-1, SHA-224/256/384/512, and SHA3-224/256/384/512 digests. HMAC, PBKDF2, and HKDF accept sha1, sha224, sha256, sha384, sha512, sha3-224, sha3-256, sha3-384, or sha3-512 as the hash name. PBKDF2 and HKDF accept an output length and return an R failure for an invalid hash, iteration count, or length.
Thread limit
GD.async.set_max_threads(max) sets the limit on OS threads managed by gd and returns the previous value. The default is 10000. Exceeding the limit terminates the process. Lowering it below the current count also terminates it. Threads created directly by external libraries are not counted.
Web framework
Register routes, static files, templates, and middleware on the router returned by GD.web.app(). A website that returns HTML and a Web API that returns JSON are built the same way. Start with a site that returns one HTML page.
var app := GD.web.app()
func home(_req):
return GD.web.html("<h1>gd</h1><p>hello</p>"), null
func hello(req):
return GD.web.json({"message": "hello", "ip": req.ip}), null
func main():
app.static("/assets", "res://public")
app.route("GET", "/", home)
app.route("GET", "/api/hello", hello)
app.listen(8080, "127.0.0.1")!
return 0
gd --strict --allow-net=127.0.0.1:8080 serve main.gd
serve keeps the process alive after main() returns. Run with gd main.gd, the process exits right after it starts listening. There is no "listening" signal at startup, so confirm by connecting with a browser or curl.
Routes and replies
route(method, pattern, handler) binds an HTTP method and a path to a handler. :name in the pattern arrives in req.params["name"]. A handler receives a GDWebRequest. It reads only the needed body through req.read(), bytes(), text(), json(), or save(). A body sent by an HTML form becomes a dictionary with GD.http.decode_query(req.text()?).
The value a handler returns becomes the reply.
| Returned value | Reply |
|---|
GD.web.html(body), GD.web.view(path, data) | HTML |
GD.web.json(data) | JSON |
GD.web.text(body), GD.web.bytes(body, type) | Text, or any media type |
GD.web.stream(producer) | A body written a little at a time. See "Web operations and advanced features" |
GD.web.redirect(to) | 302. to is limited to a path on the same site. Set away to true to send elsewhere |
GD.web.not_found() | 404 |
| A string | 200 as text/plain |
A dictionary without body | 200 as JSON |
null | 204 |
A failed R or an Err | Status by kind. Err.NOT_FOUND is 404, Err.INVALID_DATA is 400, others 500 |
Route handlers and middleware may return a Signal, including after await. Processing resumes when it completes: no arguments become null, one argument becomes that value, and multiple arguments become an Array. An unavailable Signal goes through the error handler. Pending subscriptions are removed when the request ends or the app stops.
- text and html take the status as the second argument; bytes takes it after the media type.
GD.web.header(reply, name, value) adds a header to a reply.
GD.web.guard(reply) adds the defensive headers such as X-Content-Type-Options, X-Frame-Options, and Content-Security-Policy at once.
- The reason for a failure is not written to the body by default. It is shown only while
app.show_errors(true) is set during development.
req.path is the path with each segment decoded once. req.target is the original text, keeping percent escapes and the query. %2F does not become a path separator.
- A request whose percent-decoded result is not valid UTF-8 or contains control characters gets a 400.
- Do not modify values passed to
GD.web.json() or view() until the reply has been sent.
The router also accepts the following.
| Registration | Purpose |
|---|
app.static("/assets", "res://public") | Answer GET under the prefix with files from the directory. The media type comes from the extension, and nothing outside the directory is served. Write the index of / as a route |
app.group("/api", [middleware]) | A route group with a shared prefix and middleware. The result has route() and use() |
app.fallback(handler) | Requests matching no route. Return the 404 page here |
app.on_error(handler) | The reply when a handler returns a failure |
app.after(handler) | Reshape the reply before sending. Receives func(req, reply) and returns it with headers added |
Middleware
Middleware is a function called before the handler. It receives a GDWebRequest, returns null to continue, or returns a reply to stop there. An object with handle(req) also works. Pass values to later stages with req.keep(name, value) and read them with req.kept(name).
| Registration | Scope |
|---|
app.pre(mw) | Before route selection. Every request |
app.use(mw) | After route selection. Every route. Can read req.params |
group.use(mw) | Routes in that group |
app.route(method, pattern, handler, [mw]) | That route only |
Input validation is middleware too. GD.web.json_body(rule), GD.web.query(rule), and GD.web.params(rule) check the body, query, and path values, and put the values that pass into req.valid("body"), req.valid("query"), and req.valid("params"). Rules are built from GD.web.text_rule(), int_rule(), number_rule(), bool_rule(), list_rule(), and object_rule(), with GD.web.optional() and GD.web.one_of() for omission and choices. Query and path values are strings, so check them with text_rule() and convert with to_int() when needed.
var app := GD.web.app()
func show(req):
var params := req.valid("params")
return GD.web.json({"id": params.id}), null
func main():
app.route("GET", "/posts/:id", show, [GD.web.params(GD.web.object_rule({"id": GD.web.text_rule({"min": 1, "max": 20})}))])
app.listen(8080)!
return 0
The built-in middleware are GD.web.sessions(), GD.web.csrf(), GD.web.jwt(), and GD.web.rate(). The authentication section uses them.
HTML templates
As pages grow, move the HTML into template files and render them with GD.web.view(path, data). The template language is gdhtml, a micro template with Mustache syntax. It handles {{name}}, {{{html}}}, #if, #unless, #each, #with, else, and {{> header}}. Using {{> header}} from views/page.html reads views/partials/header.html at the same level.
<!-- views/page.html -->
{{> header}}
<main><h1>{{title}}</h1></main>
<!-- views/partials/header.html -->
<header><a href="/">gd app</a></header>
func page(_req):
return GD.web.view("views/page.html", {"title": "Top"}), null
Double-brace values are escaped by the context they appear in. The template author is trusted, the values inserted are not.
| Context | Handling |
|---|
| HTML body, quoted and unquoted attributes, attribute names | HTML escape |
href="{{url}}" | Relative URLs and http, https, mailto pass. data-href is treated the same |
href="/work/{{path}}", href="/?q={{query}}" | Paths are normalized keeping separators, query values are percent-escaped |
onclick, script body | Encoded as JSON in a form where </script> cannot break the structure, even for application/json |
style | Safe single CSS values and CSS strings and URLs pass |
| Dangerous URLs, srcset, CSS values, attribute names | Replaced with #ZgdunsafeZ or ZgdunsafeZ without failing the whole page |
- Triple braces
{{{html}}} are the only unescaped entry, and they work only in the HTML body. Pass only fixed HTML or a sufficiently checked value.
- A double brace cannot be marked "checked" to skip escaping.
- A template whose branches or
each iterations end in different contexts, an unclosed tag, or an ambiguous URL or JavaScript context fails to render.
- Template size has no fixed limit. Only the depth of recursive partials is limited, to 100000.
- Do not modify the dictionary you passed until rendering finishes.
For a server that renders the same template repeatedly, parse it once at startup with GD.html.template(source, partials)?, then call execute(data)? on the returned value from each request. The parsed value is immutable and can be used by several requests at once. execute_bytes(data)? produces UTF-8 bytes directly, so they can be returned as is with GD.web.bytes(body, "text/html; charset=utf-8").
Authentication and CSRF
Login state is held by GD.web.sessions(). issue(value) creates a session ID, and the value of cookie(id) is returned as Set-Cookie. On routes that carry the same store as middleware, the value behind the cookie's ID arrives in req.kept("user"), and a missing session is a 401.
var app := GD.web.app()
var sessions := GD.web.sessions()
func login(req):
var form := GD.http.decode_query(req.text()?)?
var user := str(form.get("user", ""))
if user.is_empty():
return GD.web.text("user is required", 400), null
var reply := GD.web.redirect("/me")
return GD.web.header(reply, "Set-Cookie", sessions.cookie(sessions.issue(user))), null
func me(req):
return GD.web.text("hello, " + str(req.kept("user"))), null
func main():
app.route("POST", "/login", login)
app.route("GET", "/me", me, [sessions])
app.listen(8080)!
return 0
cookie(id) sets Secure and HttpOnly. If the cookie does not arrive during development without TLS, use cookie(id, false). Log out with drop(id) and clear_cookie(). Sessions live in one process, so with several processes under --workers use JWT or an external store.
Attach GD.web.csrf() to write paths that authenticate with cookies. Requests other than GET, HEAD, and OPTIONS need the browser's Sec-Fetch-Site: same-origin. When old browsers or non-browser clients must be accepted, choose GD.web.csrf({"allow_missing": true}) and combine it with separate token verification.
var app := GD.web.app()
var sessions := GD.web.sessions()
func save_email(_r):
return "saved"
func main():
app.route("POST", "/account/email", save_email, [GD.web.csrf(), sessions])
app.listen(8080)!
return 0
When JWT is used as a login session, revoke issued tokens on password change and logout. check is called after the signature and standard claims are verified, and authentication passes only when it returns true. For example, put the user's ver in the token and increment the stored version on password change. With several workers, compare against something like a cache synced from a shared DB, not a per-process dictionary.
func token_auth(key, versions):
return GD.web.jwt(key, {"check": func(claims):
return versions.get(claims.get("sub", ""), -1) == claims.get("ver", -2)
})
To limit per IP behind a reverse proxy, list the proxy's IPs or CIDRs in trusted_proxies. gd strips trusted proxies from the right end of X-Forwarded-For and uses the first untrusted IP as the key. X-Forwarded-For is ignored when trusted_proxies is unset and when it comes from an untrusted peer, so a client cannot forge its own IP. IPv4 and IPv4-mapped IPv6 are matched as different things, so use an IPv6 CIDR to trust mapped addresses. Proxy settings with zones are rejected.
var per_ip := GD.web.rate({"limit": 60, "trusted_proxies": ["127.0.0.1", "172.18.0.0/16"]})
Shutdown
Wait for shutdown with app.shutdown(context). It stops accepting new connections and keep-alive, then waits for in-flight requests. Past the deadline it returns Err.TIMED_OUT but does not kill in-flight requests. Use app.stop() when every connection must close immediately.
func close(app):
var context := GD.async.context().with_timeout(10.0)
var stopped := app.shutdown(context)
if not stopped.ok:
app.stop()
A handler can observe request completion and disconnection through req.context. with_cancel() and with_timeout() return a child context without changing the parent, and the parent's cancellation reaches the child. To make HTTP, database, process, and other waits cancelable, wrap them with with_context(), passing the context first. The operation result is returned when it finishes first; when the context finishes first, the operation is canceled.
func load(req, db):
var result = await GD.async.with_context(req.context, db.query_async("SELECT * FROM posts"))
return result
Web operations and advanced features
Limits and large uploads
When handling large bodies or long handlers, set the limits explicitly with limits() before listening.
func main():
var limited_app := GD.web.app()
limited_app.limits({"header_bytes": 1048576, "header_values": 500, "header_timeout": 15.0, "body_timeout": 10.0, "job_timeout": 30.0, "jobs": 128})
return 0
To accept a 1 GB ZIP, put a per-request limit on it and stream it to a writable mount.
func main():
var app := GD.web.app()
app.limits({"body_timeout": 600.0})
app.route("POST", "/upload", func(req):
req.limit(1000 * 1000 * 1000)
req.save("uploads://package.zip")?
return GD.web.text("saved")
)
return 0 if app.listen(8080, "127.0.0.1").ok else 1
gd --strict --allow-net=127.0.0.1:8080 --mount=uploads=/srv/uploads:rw serve main.gd
Bodies and memory are handled as follows.
| Target | Handling |
|---|
| Request body | No default size limit. The handler starts right after the header, and the body is read from the connection only as the handler reads it |
read(), save() | Stream the body. An empty successful read() is EOF. save() never holds the complete body in memory |
bytes(), text(), json() | Read the whole remaining body into memory. Use save() for large bodies. text() is limited to what fits in a String |
req.limit(bytes) | Per-request body limit. Overflow is returned as a failure to the body-reading operation |
| Request header | Default 1 MiB. The line count is limited only when header_values is set. Trailers 4096 bytes |
| HTTP client response header | Up to 10 MiB |
| Slow connections | Only that connection waits. Other connections are not affected |
| Extra reply headers | No fixed count or aggregate limit. Only invalid names and values are dropped |
| Sessions and rate limits | Shared within a process, not across --workers. Use an external store such as a DB when sharing is needed |
| Session values | String and integer identifiers. Retention counts are set with total and per_user |
| HS256 JWT | Key at least 32 bytes. JSON and signature validity are checked |
| Rate limit key | Retention count is set with keys |
| HTTP status | 100..999. Out of range is sent as 500 |
| Port | 0 is allowed for listening and as the search start of GD.net.free_port(). Targets and is_free() take 1..65535 |
| Query string | GD.http.decode_query() reports a bare semicolon and a broken percent escape as failures |
Streaming bodies
GD.web.stream(producer, length=-1, type="application/octet-stream", status=200) sends only what producer(writer) writes to the GDWebWriter. It never joins the whole body in memory. The producer may await, and it finishes by returning void or an R.
GDWebWriter | Behavior |
|---|
write(data, offset=0, count=-1) | Sends a range of a byte array and returns the accepted bytes. When sending is backed up, it waits until it progresses |
write_text(text, offset=0, count=-1) | Sends a range of a string as UTF-8. Offset and count are in characters; the result is in bytes |
flush() | Waits for preceding writes to be sent. A disconnect shows up as an error here and as req.context cancellation |
- A stream is single-use. Create a new one for each response. Finish reading the incoming body before returning the stream.
length is the number of bytes to send. If the declared and actual lengths differ, the connection is closed. Unknown length (-1) uses DATA frames on HTTP/2, chunked framing on HTTP/1.1, and connection close as the end on HTTP/1.0.
- HEAD and statuses that cannot carry a body never call the producer.
- Long-waiting producers should observe
req.context cancellation.
- One write does not necessarily correspond to one chunk. An empty string or empty byte array does not end the body.
HTTPS and HTTP/2
Start HTTPS with app.listen_tls(8443, "cert://chain.pem", "cert://key.pem", "127.0.0.1") and check the returned R. Mount the certificate directory read-only with --mount cert=/path/to/certs:r. Pass a PEM chain and an unencrypted private key. When key validation fails, no port is opened.
TLS 1.2 and 1.3 are supported, and ALPN selects HTTP/2 or HTTP/1.1. Each HTTP/2 stream proceeds independently, and canceling one does not close the others. header_timeout also applies to an incomplete handshake. For requiring client certificates, see the TLS tables in "TCP and UDP".
gzip compression
GD.data.gzip_writer(writer, level=-1) creates a GDGzipWriter that gzips the bytes written to it and passes them to the writer below. The writer below can be a GDFileStream, a TCP connection, or a GDWebWriter. The whole body is never held in memory.
| Item | Details |
|---|
| Methods | write(bytes), flush(), close(), and reset(writer). Each returns R |
level | -2 (Huffman only), -1 (default), and 0..9 |
close() | Finishes the gzip trailer. It does not close the writer below |
reset(writer) | Clears errors and reuses the compressor at the same level |
header | name and comment (non-NUL Latin-1), extra (up to 65535 bytes), mod_time (Unix seconds), and os (default 255). Set it before the first write |
For HTTP, return GD.web.header(GD.web.stream(producer), "Content-Encoding", "gzip"); the producer creates the compressor, writes, and returns the result of close(). Checking Accept-Encoding and setting Vary are up to the caller. Do not compress secrets together with external input, and do not apply it to an already compressed body or a partial response.
Listen address and port
For an IPv6-only localhost listener, use app.listen(8080, "::1")!. Under strict use --allow-net=[::1]:8080, and connect to http://[::1]:8080/. ::1 and 127.0.0.1 are separate listeners, and both differ from ::, which means every interface.
To let the OS pick a free port, read app.port() right after app.listen(0). The number is obtained while holding the listener, so no other process can take it. Under strict the chosen port cannot be limited ahead of time, so allow the whole host, as in --allow-net=127.0.0.1. GD.net.free_port() and is_free() are momentary diagnostics, not a way to reserve that number.
HTTP client connections
- HTTPS uses HTTP/2, and concurrent requests to the same origin share one connection. Peers without HTTP/2 and plain HTTP use HTTP/1.1.
- An HTTP/1.1 connection is reused for the same origin after its body is read to the end. Idle connections are kept up to 100 overall, 2 per origin, for 90 seconds.
- If a reused connection closes just after reuse, only a safely replayable method is retried once on a fresh connection.
- On HTTP/2, only requests the peer marks as unprocessed are replayed, up to seven times with growing intervals. The request deadline and cancellation still apply.
Web settings
The settings passed as a dictionary to GD.http.fetch() and the GD.web functions, with their defaults. Times are seconds and sizes are bytes.
| Entry | Setting and default | Meaning |
|---|
GD.http.fetch | method="GET", headers={}, body=null | HTTP method, request headers, request body |
| same | timeout=30.0, max_body=0 | Seconds for the whole request and bytes of the response body. 0 is unlimited |
| same | save="", sha256="" | Stream a 2xx body to save, returning an empty body. sha256 requires save, is 64 hex digits, and only a matching completed file is placed |
| same | authority="host:port" | Request target for CONNECT only |
GD.cli.run | timeout=0.0, output=true | Seconds before giving up on the child process, and whether to collect output |
GDWebApp.limits | jobs=0, job_timeout=0.0 | Number of async handlers kept and seconds. 0 is unlimited |
| same | header_timeout=0.0, body_timeout=0.0 | Seconds to finish receiving request header/body. 0 is unlimited |
| same | header_bytes=1048576, header_values=2147483647 | Header bytes including the request line, and the header line count |
GD.web.jwt_sign | ttl=900 | Seconds used to fill iat/exp. 0 does not add them |
GD.web.jwt / jwt_verify | leeway=0.0, require_exp=true | Clock tolerance in seconds, and whether exp is required |
| same | iss="", aud="", keep="jwt" | Issuer/audience match when non-empty, and the name kept on the request |
| same | check=Callable() | Revocation check receiving claims after signature verification. When set, only true passes |
GD.web.sessions | total=1024, per_user=3 | Sessions per process, and per user |
| same | idle=1800, life=43200 | Idle and maximum lifetime in seconds |
| same | cookie="sid", keep="user" | Cookie name and the name kept on the request. The cookie name uses ASCII token characters |
GD.web.rate | limit=60, window=60.0 | Count per key and the fixed window in seconds |
| same | keys=10000, key=Callable() | Keys kept per process and the key selector |
| same | trusted_proxies=PackedStringArray() | IPs or CIDRs of proxies whose forwarded IP is trusted |
GD.web.csrf | allow_missing=false | Whether to allow state changes from clients without Fetch Metadata |
GD.web.text_rule | min=0, max=4096 | Text length in characters |
GD.web.int_rule | min=-9223372036854775808, max=9223372036854775807 | 64-bit integer range |
GD.web.number_rule | min=-1e308, max=1e308 | Finite float range |
GD.web.list_rule | min=0, max=1024 | Element count |
GD.web.object_rule | extra=false | Whether to keep undeclared fields |
GDWebApp.limits accepts only the six listed setting names and rejects misspellings and body_limit.
Numeric settings accept the following ranges. A value outside the range fails when set.
| Setting | Accepted range |
|---|
jobs | 0..2147483647. 0 is unlimited |
header_values, session total/per_user, rate limit/keys | 1..2147483647 |
job_timeout, header_timeout, body_timeout | Finite 0..9223372036.854776 seconds. 0 is unlimited |
session idle/life | 1..9223372036 seconds |
header_bytes | 1..2147479551 bytes. Separate from the body |
req.limit, GD.http.fetch.max_body | 0..9223372036854775807 bytes. 0 for max_body is unlimited |
ttl | 0 or more |
leeway | Finite, 0 or more |
Database
The client returned by GD.database.client() handles SQLite and PostgreSQL with the same code. Switching from the embedded SQLite in local development to PostgreSQL in production is done through the driver passed to open().
func main():
var local := GD.cli.env("DB_DRIVER", "sqlite") == "sqlite"
var db := GD.database.client()
db.open({
"driver": "sqlite" if local else "postgres",
"path": "user://app.sqlite3",
"host": "127.0.0.1",
"database": "app",
"user": "app",
"password": GD.cli.env("PGPASSWORD", ""),
})?
db.query("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")?
db.query("INSERT INTO users (id, name) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING", [1, "ada"])?
var out := db.query("SELECT id, name FROM users WHERE id=$1", [1])?
print(out.rows[0].name)
db.close()
return 0, null
Table creation, INSERT, and SELECT all go through the one query(). It yields a dictionary with columns, rows, and tag, where rows is an array of dictionaries keyed by column name. In the example, out.rows[0].name is ada. SQL values are bound in order as $1, $2, and the spelling is the same on both drivers. SQL is not translated, so use SQL that works on both.
| Method | Purpose |
|---|
query(sql, args) | Collect and return the whole result |
query_row(sql, args) | Return only the first row. Err.NOT_FOUND when there is no row |
query_rows(sql, args) | Open GDDatabaseRows and read one row at a time. For large results |
stats() | Connection count, in use, idle, wait count, wait duration, and cumulative close counts by reason |
Advance query_rows() with while rows.next(). scan() returns a dictionary keyed by column name and values() returns an array in column order. After next() returns false, inspect err(). Call close() when stopping early.
func list_users(db):
var rows := db.query_rows("SELECT id, name FROM users ORDER BY id")?
while rows.next():
var user := rows.scan()?
print(user.id, " ", user.name)
if rows.err() != null:
return R.err(rows.err())
return R.ok()
On a constraint violation, result.e.info carries machine-readable details. violation is one of duplicate, not_null, or foreign_key, and columns lists the related column names. On PostgreSQL, code, table, and constraint are included when the server returns them. Values themselves are never kept in info. A failure reported by SQLite itself keeps source="sqlite" and its extended source_code. SQLite's foreign key message has no column names, so columns is empty there.
func save(db):
var saved := db.query(
"INSERT INTO users(id,name) VALUES($1,$2)",
[1, "ada"])
if not saved.ok and saved.e.info.get("violation") == "duplicate":
var columns := saved.e.info.get("columns", PackedStringArray())
print("duplicate columns: ", columns)
Transactions and migrations
To make several updates one success or failure, use transaction(). The callback receives a GDDatabaseTx pinned to one connection. Returning a successful R commits, returning a failed R rolls back.
func save(db, id, title):
return db.transaction(func(tx):
tx.query("INSERT INTO posts(id,title) VALUES($1,$2)", [id, title])?
tx.query("UPDATE counters SET value=value+1 WHERE name='posts'")?
return R.ok(id)
)
- Use the given
tx in the callback and always return an R. During a transaction, query() on the original client and a nested transaction are rejected.
- A commit failure is returned as a failure.
- Close or cancel before COMMIT begins rolls back; after it begins, the connection closes once the result is settled.
- After the callback finishes, a retained
tx no longer accepts new SQL.
To apply a schema in order, pass an array of statements to migrate() instead of splitting SQL on semicolons. If one statement fails, everything rolls back. On success it returns the number of statements applied. Versions and checksums are managed by the application.
func migrate(db):
return db.migrate([
"CREATE TABLE posts(id INTEGER PRIMARY KEY, title TEXT NOT NULL)",
"CREATE INDEX posts_title ON posts(title)",
])
Advanced database features
Driver differences
| Item | SQLite | PostgreSQL |
|---|
| Suited to | Local development, a single process | Production, crash resilience, several workers |
| Connection | One per client. Journal and temporary tables live in memory | A pool of up to max(4, CPU count) by default. Set a maximum as in pool=25 |
| Extra entries | GD.database.sqlite.open() for short work done in place | GD.database.postgres for batched sends, arrays, and JSONB |
| Notes | A database with existing -journal, -wal, or -shm files must be recovered or checkpointed with regular SQLite before opening | Hosts other than loopback verify the TLS certificate and host name by default. Loopback defaults to no TLS |
open() on GD.database.postgres.client() and GD.database.redis.client() takes the target as arguments, in the form open(host, port, opts).
SQLite concurrency
query() calls on one client run in arrival order. Separate clients proceed concurrently, while writes to the same database file follow SQLite locking. The GDSQLiteDB and GDSQLiteStatement returned by GD.database.sqlite.open() are a synchronous API that runs directly on the caller. Use them only for short work and never concurrently. Use GDDatabaseClient for concurrent work.
PostgreSQL connections and types
- The pool creates no connection until the first query and grows only for demand up to the maximum. Queries after the maximum is reached wait in arrival order.
- An ordinary
query() is also sent onto a busy connection (pipelining). Results on one connection return in the order sent.
- Transactions and
query_rows() reserve one connection. For work that uses connection-local state, use the transaction API instead of sending a standalone BEGIN.
- Use
query_many, fetch_many, or exec_many to send several SQL operations together on one connection.
- Cancellation and deadline expiry notify the caller at once, but do not guarantee that the SQL stopped on the server. Other queries are not interrupted.
wait_count in stats() counts waits to acquire a connection and excludes response waits inside a pipeline.
- Authentication follows the server's request with SCRAM-SHA-256 or MD5. Pin the method with
auth="scram" or auth="md5". MD5 is for older servers. A cleartext password needs explicit permission.
- JSON and JSONB columns are read by the same rules as JSON in "Files and data" and preserve 64-bit integers. Ambiguous values such as duplicate names return the original JSON string.
bool[], int[], bigint[], and text[] preserve element types, nulls, and nested dimensions. Arrays with explicit lower bounds return the original text.
- Connections request UTF8. A server-reported change to another client encoding closes the connection with an error. The SQL that made the change may already have executed.
Redis connections
- The TLS choice is the same as PostgreSQL.
timeout on open() sets the connect and response deadline in seconds.
- Pool
open() only configures the destination; network activity begins with the first query().
- A connection in use is held exclusively until it is returned, and callers wait in arrival order when none is free. Canceling a waiter does not affect other calls; canceling an active call closes its connection.
size() counts connections, including those connecting; in_flight() counts unfinished calls, including acquisition waiters.
Database settings
The settings passed as a dictionary to open(), with their defaults.
| Entry | Setting and default | Meaning |
|---|
GDDatabaseClient.open | driver="postgres", path="" | Driver and SQLite path. SQLite needs user://... or :memory: |
| same | host="127.0.0.1", port=5432 | PostgreSQL target |
| same | pool=0 | PostgreSQL maximum connections. 0 means max(4, CPU count). Unused by SQLite |
| same | max_rows=0, max_bytes=0 | Rows and bytes per result collected by query(). 0 is unlimited. Not applied to query_rows() |
GDPostgresClient.open | user="postgres", database="postgres", password="" | Credentials and database name |
| same | connect_timeout=15.0, timeout=0.0 | Connect and query seconds. Waiting for a pool connection counts toward the query time. 0 is unlimited |
| same | auth="any", allow_cleartext_password=false | Pin the method with auth="scram"/"md5". A cleartext password reply only when explicit |
| same | tls=<decided by host>, ca="" | External hosts use verify-full, loopback disable. A CA file only when explicit |
GD.database.sqlite.open | busy_ms=5000, max_ms=0 | Lock wait and execution deadline in milliseconds. 0 is unlimited |
| same | max_rows=0, max_bytes=0 | Rows and bytes per result. 0 is unlimited |
GDRedisClient.open | password="", timeout=10.0 | Password, and connect and response deadline in seconds. 0 is unlimited |
| same | tls=<decided by host>, ca="" | The same TLS choice as PostgreSQL |
GD.database.postgres.pool | size default 0; 0 or 1..2147483647 | 0 means max(4, CPU count) |
GD.database.redis.pool | size default 0; 0..2147483647 | Maximum connections. 0 is unlimited. Connections being opened at once are capped at ten times the CPU count, or at the maximum when one is set |
GDRedisPool.open | pool_timeout=timeout+1.0 (30 seconds when timeout is 0) | Deadline for waiting for a free connection. An explicit 0 is unlimited |
A query() over max_rows or max_bytes fails only that query. The whole connection is closed when ordering is lost through a deadline or a corrupt reply.
| Setting | Accepted range |
|---|
max_rows, max_bytes, busy_ms, max_ms | 0..2147483647 |
| Bound values | 65535 for PostgreSQL, and the engine's variable limit for SQLite. query_many has no fixed item count |
| One PostgreSQL send | SQL and bound strings are counted as UTF-8 bytes, up to roughly 1 GiB |
| One Redis send | Server-configured limits apply |
| PostgreSQL and Redis port | 1..65535 |
| Seconds | Finite 0..9223372036.854776 seconds. 0 is unlimited |
Scheduled jobs
A job that runs once at a fixed time is an ordinary script, called from the OS's cron or a systemd timer. gd needs no resident scheduler for it.
func collect():
var now := GD.time.to_iso(GD.time.now())
GD.file.append_text("store://log.txt", now + "\n")?
return 0, null
func main():
collect()?
return 0, null
gd --strict --mount store=/var/lib/app:rw collect.gd
A job that loops on its own interval is passed to GD.async.spawn() and kept resident with gd serve. Work passed to spawn() keeps running after main() returns.
func every(sec, fn):
while true:
await GD.async.sleep(sec)
fn.call()
func collect():
print(GD.time.to_iso(GD.time.now()))
func main():
var _job := GD.async.spawn(every.bind(60.0, collect))
return 0
gd serve schedule.gd
Stop it by ending the process. It is resident like a Web server, so serve is needed here too.
Official extension modules
The core stays small. Features specific to an external service are added as GDScript packages or GDExtensions only to projects that need them.
| Entry | Purpose | API and setup |
|---|
Discord | Pure-GDScript text bots on the Discord Gateway and REST | Discord Bot |
GDMemcached | Cache client reusing TCP connections | Memcached |
GDSupabase | Database and Auth client | Supabase |
Each document lists public classes, methods, return values, limits, and strict-mode examples. Because they are optional, they are not part of the API reference generated from the core alone.
- Extensions added with
gd add are trusted and loaded at startup, so no flag is needed. --allow-net for their target is still needed.
--allow-ext and --deny-ext apply when a script loads one while running with GDExtensionManager.load_extension().
- An added extension runs with the same privileges as the process, so pin the versions you trust in
gd.lock and commit it.
Packages and distribution
When scripts multiply or you start using other packages, create gd.json with gd init. Dependencies are pinned with gd.json and gd.lock.
gd init
gd search discord bot
gd add gd:@scope/script-package@^1.0.0
gd add ext:@scope/name@^1.0.0
gd add short-name https://example.com/module.gd
gd install --frozen
gd task test
Using packages
An installed package is read from pkg://<alias>/, using the alias chosen by the consumer.
const Hello := preload("pkg://hello/mod.gd")
pkg:// points into the per-user shared cache and copies nothing into the project.
- A dependency named in
gd.json but absent from the cache is fetched on the first run. Under --strict the registry needs --allow-net.
- The default alias of
gd add is the package name with - and . turned into _, so it is an identifier. Aliases that are engine classes or keywords are refused.
- Commit
gd.json and gd.lock. gd init writes pkg/ into .gitignore.
--frozen does not change the lock. For an offline target, fetch first where a network is available, and add --cached-only.
- When install, add, or update fails partway, project files and the lock are restored.
- The lock is bound to its registry. Switching to another registry requires explicit lock migration.
Short import syntax
@import is the short form of const Name = preload(...).
@import greet # an alias: pkg://greet/mod.gd, bound as greet
@import greet/style as Style # a script inside the alias
@import "./util.gd" as Util # explicitly relative file
@import "./net/client.gd" # a file below this directory
@import "../shared/util.gd" # quote paths that start with . or a scheme
@import "./net/mod.gd" as n
- Unquoted names resolve only aliases declared in the
imports of gd.json. Files with the same name are not searched.
- Quote relative files, starting with
./ or ../.
- Without
as, the identifier is the last segment as written, and a directory holding mod.gd binds its directory name.
gd fmt keeps @import as it is.
- Upstream Godot does not know
@import, so files shared with Godot should spell out const and preload.
Creating a package
A package is one project rooted at its gd.json. gd init @scope/name seeds mod.gd and a test, gd test runs it, and gd publish releases it.
{"name":"@scope/hello","version":"1.0.0","main":"src/mod.gd","include":["src"]}
gd publish
gd add hello gd:@scope/hello@^1.0.0
- The entry is
mod.gd. For multiple files, list files or directories in include.
- The main file's directory becomes the package root, so relative preloads inside the package keep working.
- A package may use other registry packages through the
imports of its own gd.json. gd publish records those imports in the registry.
class_name may be published. Installation checks for conflicts between classes of the same name and rolls everything back on a conflict.
- Setting
godot to true in gd.json is the author's declaration that the package runs on upstream Godot without gd's own API, and gd search marks it [godot].
A package under development is added from a local path with gd add ../path. Its alias comes from the name in its gd.json. The checkout is copied under pkg/<alias>/ and copied again on the next run whenever its content fingerprint changes. Files starting with ., pkg/, tmp/, subdirectories holding a gd.json, and token are not copied. gd publish turns a local import into its registry range when the target's gd.json has name and version, and refuses it otherwise.
Dependency resolution
gd install resolves the whole dependency graph. It picks a version in this order: the version gd.lock pins, a version already chosen this time that satisfies the range, then the newest match in the registry.
gd.lock also records the resolved imports configuration. A run after a configuration change uses the same resolver instead of silently loading an outdated version. --frozen rejects mismatched requests. When multiple aliases name one package, the lexicographically first alias selects its copy directory.
- Pure GDScript packages coexist as distinct versions.
- A native extension loads only once per process, so it is unified to one version. If the ranges cannot agree, it stops before anything is fetched.
- There is no mechanism for plugins that share one host instance (peer dependencies).
- The canonical path of a registry package is
pkg://@scope/name@version/. pkg://<alias>/ expands to it through the imports of the package the script belongs to. The same alias may name different versions in different packages, and one version is one script however it is reached.
gd.lock also records each package's resolved imports, and gd info lists them.
gd remove and gd update drop what no package uses any more from gd.lock and pkg/.
- A change in search ranking does not affect installation or lock verification for a known package.
Sharing a location with Godot
To share a project with tools that only read res://, such as upstream Godot, set "place": "project" in gd.json. Packages are copied under pkg/<alias>/, and both pkg:// and res://pkg/ point there. A package that only other packages use goes under pkg/@scope/name@version/.
- Where
project.godot exists, place defaults to project and no .gitignore is written. Commit pkg/ so teammates without gd can open the project.
- Installation rewrites
res:// references written in preload, load, and extends to the placement. Strings, comments, and paths built at run time are not rewritten.
place only selects where files live. It does not convert gd's own API or syntax for Godot. Shared source should use standard syntax and relative preloads.
Native extension packages
- A native extension needs real files to load, so it lives under
pkg/<alias>/ whatever place is.
- A script may only name classes of the extensions its own package imports with
ext:: the project's gd.json for project scripts, the package's own imports for package scripts.
- An extension outside the registry is usable by project scripts only.
- A registry package's extension that registers a class missing from its manifest's
[classes] stops startup.
- Fetch on the target OS, or run
gd compile on the target OS.
Settings and environment variables
gd.json has these ten settings.
| Name | Written by gd init / when omitted | Meaning |
|---|
name | my-tool / required | Project name. Publishing needs @scope/name |
version | 0.1.0 / required | Package version |
tasks | run and test / none | Commands invoked by gd task |
imports | {} / {} | Alias and dependency source. A published package may name registry packages only |
registry | omitted / environment or the public registry | Registry URL pinned to the project |
main | omitted / mod.gd | mod.gd or .gdextension entry published |
include | omitted / main only | Files or directories inside the main directory included in a pure GDScript package |
place | omitted / cache, or project beside project.godot | Where packages live. project copies them under pkg/ |
godot | omitted / false | Declares a package that runs on upstream Godot without gd's own API |
description | omitted / empty | Description shown in the registry |
gd reads these environment variables. A script that reads the environment needs the names allowed with --allow-env.
| Variable | Purpose |
|---|
GD_CACHE_HOME | Package cache root. Defaults to gd in Windows LocalAppData. On macOS/Linux, uses an absolute XDG_CACHE_HOME plus /gd, or .gd under the home directory. The OS account directory is used when HOME is unset |
GD_REGISTRY | Registry. Defaults to https://gd-cli.progsha.com/pkg. registry in gd.json wins |
GD_TOKEN | Publish token. Keep it out of config files and pass it only to the publishing process |
LC_ALL, LANG | Language of the manual shown by gd doc |
GD_REGISTRY_HOST | Registry listening address. Defaults to 127.0.0.1; use 0.0.0.0 inside a container |
GD_REGISTRY_DATA | Registry storage directory. Defaults to data; use a writable mount in strict mode |
PORT | Listening port of the bundled registry tools/registry.gd. Default 8787, 1..65535 |
GD_WORKER | Internal mark set by --workers. Not a user setting |
Remote packages and registries use HTTPS. A development registry on loopback may also use HTTP. Fetched packages and native libraries are checked against the SHA-256 in the registry index. A .gdextension manifest is limited to 16 MiB, and all files in a package to 500 MiB in total.
Distributing a single executable
compile collects scripts, views, static files, migrations, dependency packages, and the target OS's GDExtensions into one executable. The target needs no cache.
gd compile -o app main.gd
./app
- Every package
gd.json names, and every package those import, is embedded.
- An embedded Web app can also stay resident with
./app serve --no-scene-tree --allow-net.
- From a local path package, files starting with
. such as .env and the token in gd.json are left out.
- Do not embed secrets in source. compile excludes
.env, but values written in source remain in the executable.
Scope and reporting
gd is a public release before the API has settled. Do not assume backward compatibility. Changes and the Godot version used as the base are recorded in the CHANGELOG. gd is not an official product of the Godot Foundation or the Godot Engine project.
Report bugs in Issues. Report vulnerabilities that should not be public through GitHub private reporting.