位元詩人 [Pascal] 網頁設計教學:用 pas2js 將 Free Pascal 程式碼轉為 JavaScript 程式碼

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

網頁原本是靜態的文件載體,現在已經變成最廣泛使用的跨平台運行環境之一。許多的軟體專案都有將特定程式碼轉譯成 JavaScript 程式碼的轉譯器,為了就是搭網頁技術這班順風車。

本文介紹將 Pascal 程式轉為 JavaScript 程式的軟體專案。或許可以藉此延續或拓展 Pascal 程式的生命週期。

注意事項

雖然 pas2js 有持續開發和維護,但 pas2js 目前只能算是堪用,尚未達到實用的程度。主要的原因是 pas2js 的文件過少。除了官方文件外幾乎沒有什麼能看的資料,碰到問題只能藉由觀看少量文件或是 pas2js 的原始碼來解決。對於實務來說開發效率過差。

經轉換後的 JavaScript 命令稿,是在 JavaScript 運行環境 (瀏覽器、Node.js) 下運行,而非原本的 Pascal 運行環境。還是得測試轉換後的 JavaScript 命令稿,確認該命令稿的行為符合預期。

除了本文所介紹的 pas2js 外,網路上還可以找到幾個類似的軟體專案。但除了本文所介紹的軟體外,其他的專案和 Free Pascal 官方團隊皆無關。

pas2js 的工作原理

pas2js 本質上是 Pascal 轉 JavaScript 的轉譯器。該軟體會掃描輸入的 Pascal 程式碼,將其轉換為等效的 JavaScript 程式碼。我們可將轉換過的 JavaScript 命令稿拿到瀏覽器或 Node.js 上執行。

要注意 pas2js 所接受的輸入是 Pascal 程式碼,無法接受二進位檔案。在 pas2js 專案中,也是以原始碼的形式提供轉換後所需的小型運行環境 (runtime environment) 和一些常見的 pas2js 函式庫,包括 jQuery。

安裝 pas2js

在 Windows 上安裝

pas2js 提供的下載點 (ftp://ftpmaster.freepascal.org/fpc/contrib/pas2js) 下載 Windows 版本的 pas2js。將 pas2js 的壓縮檔解壓縮後放至任意位置,像是 C:\ProgramData 就是一個不錯的位置。

C:\ProgramData 為例,將 C:\ProgramData\pas2js-windows-1.4.8\bin 加到 PATH 變數中,就可以直接在命令列環境中呼叫 pas2js

在 macOS 上安裝

pas2js 提供的下載點 (ftp://ftpmaster.freepascal.org/fpc/contrib/pas2js) 下載 macOS 版本的 pas2js。將 pas2js 的壓縮檔解壓縮後放至任意位置,像是 /opt 或是 $HOME/opt 就是一個不錯的位置。

筆者實測時,macOS 版的 pas2js 似乎無法正常讀取 pas2js 執行檔所在的設定檔。處理的方式是將 path/to/pas2js-macos-1.4.8/bin/pas2js.cfg 拷貝到 $HOME/.pas2js.cfg (注意設定檔的 . 前綴)。

可參考以下範例來設置:

#
# Minimal config file for pas2js compiler
#
# -d is the same as #DEFINE
# -u is the same as #UNDEF
#
# Write always a nice logo ;)
-l

# Display Warnings, Notes and Hints
-vwnh
# If you don't want so much verbosity use
#-vw

-Fu/Users/user/opt/pas2js-macos-1.4.18/packages/chartjs
-Fu/Users/user/opt/pas2js-macos-1.4.18/packages/dataabstract
-Fu/Users/user/opt/pas2js-macos-1.4.18/packages/fcl-base
-Fu/Users/user/opt/pas2js-macos-1.4.18/packages/fcl-db
-Fu/Users/user/opt/pas2js-macos-1.4.18/packages/fpcunit
-Fu/Users/user/opt/pas2js-macos-1.4.18/packages/jspdf
-Fu/Users/user/opt/pas2js-macos-1.4.18/packages/nodejs
-Fu/Users/user/opt/pas2js-macos-1.4.18/packages/rtl

#IFDEF nodejs
-Jirtl.js
#ENDIF

# Put all generated JavaScript into one file
-Jc

# end.

經筆者實測,直接在 pas2js.cfg 中寫死絕對路徑會比較簡單。 pas2js.cfg 只有在自己的系統上使用,不用考慮跨平台的議題。

在 GNU/Linux 上安裝

pas2js 提供的下載點 (ftp://ftpmaster.freepascal.org/fpc/contrib/pas2js) 下載 GNU/Linux 版本的 pas2js。將 pas2js 的壓縮檔解壓縮後放至任意位置,像是 /opt 或是 $HOME/opt 就是一個不錯的位置。

$HOME/opt 為例,將 $HOME/opt/pas2js-linux-1.4.8/bin 加入 PATH 所在的位置,即可在命令列環境中呼叫 pas2js

撰寫第一隻程式

Hello World

用編輯器新增 hello.pas 檔案,在該檔案輸入以下內容:

program main;

begin
  WriteLn('Hello World');
end.

基本上,就是 Pascal 版本的 Hello World 程式。

將 Pascal 程式碼轉為網頁前端程式

pas2js 將寫好的 Hello World 程式轉為適用於瀏覽器的 JavaScript 程式碼:

$ pas2js -Jc -Jirtl.js hello.pas

-Jc 表示將生成的 JavaScript 程式碼串接成一個大檔案。-Jirtl.js 表示將 pas2js 附帶的小型運行環境一起包進去。

撰寫一個空 (dummy) 頁面來載入轉譯出來的 hello.js 命令稿:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body>
    <script src="hello.js">
    <script>
      rtl.run();
    </script>
</body>
</html>

載入該網頁時,應該可以在開發者主控台 (developer console) 看到 Hello World 字串。

若不想把運行期函式庫 (rtl.js) 包進 JavaScript 命令稿,就要自行將 pas2js 專案所提供的 rtl.js 命令稿載入網頁中,否則轉換的 JavaScript 命令稿會無法運作。 rtl.js 命令稿位於 pas2js-platform-version/package/rtl 目錄中。

將 Pascal 程式碼轉為 Node.js 程式

在預設情形下,pas2js 轉出來的 JavaScript 命令稿適用於瀏覽器。不過,pas2js 也可轉出適用於 Node.js 的 JavaScript 命令稿。將指令修改如下:

$ pas2js -Jc -Jirtl.js -Tnodejs hello.pas

-Tnodejs 代表將目標環境設為 Node.js。

使用 Node.js 提供的直譯器即可執行轉譯出來的命令稿:

$ node hello.js
Hello World

Pascal 和其等效的 JavaScript 程式碼

pas2js 的官方文件展示了 pas2js 將 Pascal 程式碼轉出的等效 JavaScript 程式碼。由於目前 pas2js 的教學甚少,該份文件應該是目前學習 pas2js 最重要的資源。

pas2js 不支援的特性

受限於異質運行環境,pas2js 不支援以下特性 (未全部列出):

  • 記憶體管理
  • class 的解構子
  • 指標運算
  • 運算子重載
  • 泛型程式
  • object 型態
  • variant 型態

其他項目請看 pas2js 的官方文件

在 Pascal 中使用 JavaScript 程式碼

pas2js 轉換出來的命令稿會在 JavaScript 運行環境跑,和現有的 JavaScript 程式碼能夠互動是相當重要的。著眼於這個議題,pas2js 加入了一些原本在 Pascal 中沒有的語法。

在主程式中塞入 JavaScript 程式碼

由於 pas2js 的目的就是將 Pascal 程式碼轉 JavaScript 程式碼,但必要時仍然可以用 asm 區塊在 Pascal 程式碼中直接插入 JavaScript 程式碼。參考以下的實例:

program main;                                         (*  1 *)
var                                                   (*  2 *)
  s : string;                                         (*  3 *)

procedure Print(s: string);                           (*  4 *)
begin                                                 (*  5 *)
  WriteLn(s);                                         (*  6 *)
end;                                                  (*  7 *)

begin                                                 (*  8 *)
  s := 'Hello World';                                 (*  9 *)

  (* Raw JavaScript code. *)                          (* 10 *)
  asm                                                 /* 11 */
    console.log('Print from JavaScript: ' + $mod.s);  /* 12 */
    $mod.Print('Print from Pascal: ' + $mod.s);       /* 13 */
  end;                                                (* 14 *)
end.                                                  (* 15 *)

本程式的關鍵之處在第 11 行至第 14 行。在這段程式碼中,我們使用 asm 區塊置入 JavaScript 程式碼。

asm 區塊的工作原理是 pas2js 會自動忽略 asm 區塊內的程式碼,所以 asm 區塊內的程式碼會原封不動地輸出。因此,我們可以在 asm 區塊內加入任意的 JavaScript 程式碼。

在這個例子中,我們宣告變數 s,但沒有在 Pascal 程式碼中用到該變數。在預設情形下,pas2js 為了效能考量,會抹去未用到的宣告和定義。為了避免 pas2js 把變數抹去,我們將程式碼優化抹去。參考以下指令:

$ pas2js -Jc -Jirtl.js -O- -Tnodejs -omain.js source.pas

在這行指令中,參數 -O- 的作用即為關掉程式碼優化。

用 JavaScript 實作函式和程序

既然 pas2js 允許程式設計者在主程式中插入 JavaScript 程式碼,也可以用 JavaScript 來實作 Pascal 的函式和程序。使用 JavaScript 程式碼實作函式和程序的方式是在函式和程序的宣告後加上 assembler 修飾詞 (modifier)。參考以下範例程式:

program main;                                        (*  1 *)
var                                                  (*  2 *)
  s : string;                                        (*  3 *)

function Greet(const s: string): string; assembler;  (*  4 *)
asm                                                  /*  5 */
  return 'Hello ' + s;                               /*  6 */
end;                                                 (*  7 *)

procedure Print(const s: string); assembler;         (*  8 *)
asm                                                  /*  9 */
  console.log(s);                                    /* 10 */
end;                                                 (* 11 *)

begin                                                (* 12 *)
  s := 'Hello World';                                (* 13 *)

  Print(Greet('World'));                             (* 14 *)
end.                                                 (* 15 *)

在第 4 行至第 7 間,我們以 JavaScript 實作函式,該函式會將字串相接。在第 8 行至第 11 行間,我們以 JavaScript 實作程序,該程序會在終端機印出字串。

由於我們有用到變數,同樣要關掉程式碼優化:

$ pas2js -Jc -Jirtl.js -O- -Tnodejs -omain.js source.pas

使用現有的 JavaScript 函式

在先前的例子中,我們在 Pascal 程式碼中引入 JavaScript 程式碼。但我們也可以直接使用 JavaScript 函式。參考以下範例程式:

program main;                                                 (*  1 *)
uses                                                          (*  2 *)
  SysUtils;                                                   (*  3 *)

function Abs(n: real): real; external name 'Math.abs';        (*  4 *)

procedure Log(const s: string); external name 'console.log';  (*  5 *)

begin                                                         (*  6 *)
  Log(FloatToStr(Abs(-3)));                                   (*  7 *)
end.                                                          (*  8 *)

第 4 行的 Abs 函式實際上是 JavaScript 的 Math.abs 函式。由於 Math.abs 函式有回傳值,故以函式來宣告。

第 5 行的 Log 程序實際上是 Javascript 的 console.log 函式。由於 console.log 函式無回傳值,故以程序來宣告。

在函式中使用不定參數

JavaScript 可以用 Argument 物件 實作不定參數函式,pas2js 也支援這項功能。參考以下範例程式:

program main;                                 (*  1 *)
uses JS;                                      (*  2 *)

function Sum(): real; varargs;                (*  3 *)
var                                           (*  4 *)
  i : integer;                                (*  5 *)
begin                                         (*  6 *)
  result := 0.0;                              (*  7 *)

  for i := 0 to JSArguments.length-1 do       (*  8 *)
    result := result + real(JSArguments[i]);  (*  9 *)
end;                                          (* 10 *)

begin                                         (* 11 *)
  WriteLn(Sum(1, 2, 3, 4, 5));                (* 12 *)
end.                                          (* 13 *)

由於本範例程式會用到 JavaScript 物件,故我們在第 2 行引入 JS 函式庫。

在第 3 行至第 10 行間,我們以不定參數實作相加函式。在該函式中,我們從 JSArguments 物件取得參數,然後將函式逐一加總。

使用 JavaScript 的變數或常數

在先前的實例中,我們直接使用 JavaScript 函式。除此之外,我們也可以直接使用 JavaScript 變數或常數。在以下例子中,我們引入 JavaScript 內定的自然對數和圓周率:

program main;                           (* 1 *)
const                                   (* 2 *)
  E: Double; external name 'Math.E';    (* 3 *)
  PI: Double; external name 'Math.PI';  (* 4 *)

begin                                   (* 5 *)
  WriteLn(E);                           (* 6 *)
  WriteLn(PI);                          (* 7 *)
end.                                    (* 8 *)

使用 JavaScript 類別

除了使用 JavaScript 變數和函式外,我們也可以使用 JavaScript 物件。在以下範例程式中,我們引入 JavaScript 的 Date 物件:

program main;                                                     (*  1 *)
{$modeswitch externalclass}                                       (*  2 *)

type                                                              (*  3 *)
  TJSDate = class external name 'Date'                            (*  4 *)
  private                                                         (*  5 *)
    function getFullYear(): NativeInt;                            (*  6 *)
    procedure setFullYear(const value: NativeInt);                (*  7 *)
  public                                                          (*  8 *)
    constructor New();                                            (*  9 *)
    constructor New(const ms: NativeInt);                         (* 10 *)
    class function Now: NativeInt;                                (* 11 *)
    property Year: NativeInt read getFullYear write setFullYear;  (* 12 *)
  end;                                                            (* 13 *)

var                                                               (* 14 *)
  d: TJSDate;                                                     (* 15 *)
begin                                                             (* 16 *)
  d := TJSDate.New;                                               (* 17 *)
  WriteLn(d.Year);                                                (* 18 *)

  d.Year := d.Year + 1;                                           (* 19 *)
  WriteLn(d.Year);                                                (* 20 *)
end.                                                              (* 21 *)

在第 3 行至第 13 行間,我們定義了 TJSDate 類別,該類別來自 JavaScript 的 Date 物件。在撰寫 TJSDate 類別時,請參閱 JavaScript 的 Date 物件的 API 來寫。

結語

在本文中,我們展示了將 Pascal 程式碼轉為 JavaScript 程式碼的方式。若要熟練地使用 pas2js,要對 Pascal 和 JavaScript 都有一定的熟悉度,寫起來不一定會比寫原生 JavaScript 程式來得簡單。

除了用 pas2js 撰寫新的網頁程式外,我們還可以透過 pas2js 把現有的 Pascal 程式移植到網頁平台上,讓原有的 Pascal 程式的生命週期延續下去。或許這才是 pas2js 的真正益處。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。