1. 程式人生 > >代碼在線編譯器(上)- 編輯及編譯

代碼在線編譯器(上)- 編輯及編譯

rip java、 回測 builder 代碼量 methods mat 不同 too

此文已由作者姚太行授權網易雲社區發布。

歡迎訪問網易雲社區,了解更多網易技術產品運營經驗。

在線編譯器

代碼在線編譯器,即在線代碼編寫運行工具,提供給用戶在線代碼編輯、代碼提示、代碼診斷、編譯、運行等一系列從代碼編寫到啟動運行過程中必要的功能服務,以達到IDE的核心功能,應用範圍較廣,從使用場景下大致分為兩類:

一般場景
  • 功能基礎:僅基於開發語言的語法特點及常用原生庫。

  • 內容描述:此應用場景下,對一些涉及IO,諸如讀寫、外部請求等極端操作類型支持程度較高,代碼運行環境通常使用沙箱,以滿足安全性需要。

  • 應用範疇:主要的應用業務範疇有在線代碼輔助編輯工具(Tool等)、在線考試平臺(牛客網等)、算法競賽刷題平臺(leetcode等)。

特殊場景
  • 功能基礎:基於平臺提供的大量工具API,僅結合必要的常用原生庫。

  • 內容描述:此應用場景下,用戶編寫的代碼涉及的內容被限制在平臺規定的有界範圍內,代碼風格、格式、結構也需按照平臺規範進行展開,編譯器除在基本語法檢測的基礎上也會對代碼內涉及內容、方法做細致檢測,對一些涉及IO、讀寫、網絡請求等敏感操作會進行嚴格限制。由於需要使用平臺本身提供的API,故簡單的沙箱已經無法滿足需要,需要針對不同的業務特點進行特殊的代碼運行環境安全保障。

  • 應用範疇:應用方面,根據平臺工具API提供的出發點不同,業務範疇會被限制在平臺涉及的範圍內。在量化範疇內,多數量化平臺會提供Python、Java的策略代碼在線編譯功能,並提供相關API以供用戶完成量化策略開發的需要。

由於一般場景比較常見,開發及搭建的相關成熟樣例也較多,本文在此不過多進行討論。對於特殊場景,本文將結合在網易貴金屬量化平臺Java在線編譯器的相關案例,對於在線編譯部分的實現思路進行詳細闡述。

案例說明

網易貴金屬量化平臺,核心是利用在線編譯器相關原理,(目前)提供了針對貴金屬交易的相關量化策略開發功能。後文每一個部分將以此平臺為案例,結合理論總結進行案例闡述。為方便之後的闡述,現對系統基本情況作出簡單說明:

  • 業務核心說明:用戶可結合自身市場投資經驗,形成策略,以回測或實盤方式,使用歷史行情或實時行情以策略內容進行在歷某個階段或實時地模擬交易操作,輸出策略交易盈虧,以達到驗證策略、優化策略、積攢投資經驗的目的。

  • 策略:策略即“決定何種條件下觸發交易”的一段邏輯,條件判定依據除時間及商品行情外,還可能包含機器學習結果、訓練模型結果以及經濟學指標等。表現在量化平臺上是一段Java(或其他語言)代碼,代碼通過調用平臺提供的接口進行邏輯判斷以及交易操作。

  • 策略輸出:策略輸出的直接結果就是交易信號本身及交易記錄,統計出某段時間該策略總盈虧、最大回撤、夏普率等常用盈虧評價統計指標。

過程上體現為:

  • 用戶編寫策略

  • 平臺模擬交易

  • 交易結果統計

用戶編寫策略 技術分享圖片 模擬交易並統計結果

技術分享圖片

在線編輯及編譯

一個完整的在線編譯流程,是從用戶編寫的代碼開始的(當然代碼來源不僅僅局限於此),代碼從構建(編寫或組裝)到編譯直至運行,最終輸出結果或造成預期影響。流程包括

  • 代碼構建

  • 語法檢測

  • 代碼診斷

  • 代碼編譯

  • 代碼運行

  • 內容反饋

代碼構建

代碼構建,涉及到語言類型、代碼結構以及最終的代碼生成方式。

語言類型

在線編譯器平臺構建前,需明確平臺支持的語言類型。語言類型會影響到的方面:

  • 編譯方式:可歸納至以下三種類型:

    • 解釋型:解釋型語言編寫的程序,由其對應的解釋程序執行的,不會直接涉及到編譯過程,如JavaScript等。此類語言在搭建時一般可以動態的進行執行,而無需後臺程序進行繁瑣的編譯過程。在平臺架構設計時,可結合實際需要將相關代碼的處理過程直接放置於平臺上層(如瀏覽器本身),直接反饋結果,而無需將請求處理過程放置在底層,反而會把邏輯搞復雜。

    • 編譯型:編譯型語言通常功能較為強大且相對底層,需要先將代碼編譯為目標程序機器碼文件,如C、C++等,目標程序文件可脫離代碼在計算機上多次運行。此類語言的用戶代碼,需將用戶最終提交的代碼交由服務器等具體計算機進行處理後,再進行程序運行進而反饋程序運行結果。

    • 混合型:混合型語言與編譯型語言不同點在於,編譯過程不生成機器碼而生成字節碼文件,如Java、Python等,字節碼文件同樣可被加載至特殊的運行環境中多次運行但卻無法被計算器直接識別。此類語言的用戶代碼,同樣需要交由服務器等計算機進行處理,但運行時必須交由能夠提供特殊運行環境的計算機來執行。

  • 代碼風格:代碼風格,主要是需要確定代碼是否對格式有特殊的要求,從而對提示過程作出優化,且會對之後的代碼檢測過程提供便利。例如Python會對縮進有強依賴,那麽在代碼提示和用戶使用方面需要進行特殊的服務優化。

  • 代碼提示:代碼提示必須在語言類型確認後才可確定,一般的基於瀏覽器的前端在線編輯框架,對某些語言的原生API會有現成的提示,除這一部分外,如果需要提示給用戶平臺自身開發的一些額外的API,則需要對這部分額外的內容整理為代碼提示要求的格式,進行補充與導入。

代碼結構

一般場景下,對用戶代碼的結構一般沒有特殊需求,即與一般的IDE功能相同。 但是在特殊場景下,由於代碼編寫的目的相對明確,代碼中包含的內容也是有預期的,所以在用戶代碼編寫前,就可以通過固定代碼結構的方式來限制用戶代碼的編寫內容及構成,在之後的代碼檢測階段,也可以根據此固定格式來進行初步的代碼合理性檢測。

以Java為例,固定結構的內容包括:

  • 禁止指定package結構

  • 禁止類import導入

  • 必須繼承的父類

  • 必須實現的接口

  • 類唯一性

  • 必須包含的方法

  • 代碼固定位置的提示性用註釋

生成方式

代碼生成方式上,根據平臺對用戶代碼編寫過程中的不同支持方式,在交互層面,用戶生成自己代碼的路徑會有所不同,但最終結果均以生成合理代碼為目標。

量化平臺範疇中,用戶代碼用於實現對既往數據計算學習從而在未來做出決策的策略,以目前市場上一些特征較為突出的量化平臺為例,生成方式可包括:

  • 原始代碼編輯方式

技術分享圖片 (樣例圖片來源:網易貴金屬量化平臺)此種方式下,即便借助代碼提示和相關註釋說明,用戶在代碼構建過程中也會較為困難,但對於成熟程序員而言,反而自由度會相對較高。

  • 組件化組建方式

技術分享圖片 (樣例圖片來源:BigQuant)此種方式中,對可預估的代碼內容進行組件化,用戶選用其需要的組件,由平臺根據組件選擇情況負責拼接,大大降低了代碼編寫的門檻,對於特殊行業需求但非計算機技術掌握者非常友好,且代碼的合理性得到了極大的保證。

  • 可視化組件組建方式

技術分享圖片 (樣例圖片來源:BigQuant)此種方式,是組件化的更高層面的包裝,代碼編寫的門檻再一次被降低,且在表述代碼邏輯過程中有奇效。

其實在代碼生成方式上,結合不同的需求和業務領域的具體需要,還存在很多種不同的友好的生成方式。就上述三種方式而已,明顯可以看出後兩種方式在代碼生成上更為友好和可用,但在自由度上可能有所降低。

代碼生成方式上,如果代碼內容可預估、結構相對固定,在有條件的情況下,建議在提供除原始代碼編輯方式的基礎上,提供其他以組建為主體思路的代碼生成方式。組建的代碼生成方式,不但能提升用戶體驗,大幅度降低用戶使用門檻,還能夠有效降低用戶代碼出現語法錯誤及邏輯不合理的可能性。

案例說明

結合這一部分關於代碼構建的總結,案例中對應的相關部分內容如下:

  • 語言類型:Java8

  • 代碼結構:用戶策略代碼在編輯時,平臺會預先提供模板,並提供相關所有API的代碼提示。模板內包含了必須實現的接口以及必須包含的方法,並在固定流程結構過程中標記了提示用註釋。在編寫過程中,對用戶編寫並不受限,代碼檢測過程目前不在編輯過程中進行。

  • 生成方式:目前提供原始代碼編輯方式,未來計劃朝著組件化方向進行發展。

代碼檢測

用戶代碼檢測,是指在用戶代碼在診斷及運行前對其內容及語法,針對語法合法性以及構建階段預想的代碼結構進行檢測,以甄別用戶代碼是否合理。如果在平臺設計時,用戶代碼只是最終運行代碼的一部分,公共部分由系統拼裝,也可在這一檢測過程中完成拼裝過程。

代碼拼裝

代碼拼裝,即對用戶編輯部分補充其余的公共部分,這樣做既可以減少用戶需要編輯的代碼量,又能在一定程度上限定用戶代碼中出現一些意料之外的內容。

以Java為例,拼裝內容可包括:

  • package路徑:可限制最終生成的類的路徑

  • import類導入:可限制用戶能夠使用的類範圍

  • 註解:可對用戶代碼以類或方法為粒度追加其他行為

  • 通用方法:追加在運行時必須調用的通用方法(一般會置於抽象類中)

將拼裝內容以預想方式與用戶編輯部分合並為一個完整的可被檢測的代碼文件。

語法檢測

簡單的語法檢測可以直接通過識別文件進行,或直接嘗試利用診斷過程獲知文件是否語法合理,再復雜的就要結合編譯原理中的語法分析器構建抽象語法樹來進行詳細解析。

結構檢測

對照代碼構建階段的代碼結構相關內容,檢測內容包括:

  • 文件路徑是否合理(包路徑)

  • 類名合法性

  • 類是否存在必要繼承及實現

  • 是否包含必要參數

  • 是否包含必要方法

  • 是否符合其他必要固定結構

經歷上述過程後,基本可以得到獲知一份用戶代碼是否有被編譯診斷的必要性。

案例說明

結合這一部分關於代碼檢測的總結,案例中對應的相關部分如下:

  • 代碼拼裝:網易貴金屬量化平臺中,用戶只需繼承接口後,實現主體的三個方法,且在模板中對這三個方法的流程已做了詳細說明,類之外的部分是無需用戶編寫的。代碼拼裝內容包括:

    • 設置類代碼文件的package路徑至統一位置,並結合用戶信息和時間戳進行生成子路徑,防止路徑下類重名

    • 所有java.lang之外的包,只導入涉及到的部分,涵蓋計算、數據結構、時間處理等內容

    • 導入所有平臺提供的API類

  • 語法檢測:直接使用編輯工具診斷過程進行,沒有在這一部分使用到抽象語法樹。

  • 結構檢測:量化平臺上用戶代碼以類作為用戶代碼編寫的主體,而不包含其他內容,結構檢測內容包括:

    • 用戶代碼部分不得為空

    • 用戶不得自主導入類

    • 必須繼承用戶策略模板接口

    • 必須策略模板的完整實現類

    • class關鍵字唯一,類唯一,且不得包含內部類

經過初步檢測後,如果代碼檢測無誤,就可以到代碼診斷和代碼編譯,以進一步證明代碼的可用性。

代碼診斷

代碼診斷(Diagnostic),此處的診斷,指在編譯過程中,對代碼是否可運行作出檢查,並報告相關問題位置的過程。一般的IDE在Build過程中均會進行診斷,診斷過程會報告問題類型並指出問題所在行號,但並非所有診斷都會存在行號。診斷內容涵蓋:

  • 語法合法性:語句本身是否合法

  • 文件結構合法性:文件內容是否符合某語言的基本要求

  • 調用合法性:文件內涉及到的其他類或方法是否存在

代碼診斷後,也就標誌代碼文件可以在當前環境中運行,但此階段內一般不會檢查運行時錯誤。一般的代碼診斷會伴隨代碼編譯過程進行,在代碼編譯過程中通過監聽編譯過程獲得診斷信息,但非編譯型語言的代碼診斷是獨立進行的。

案例說明將與代碼編譯過程一同進行。

代碼編譯

代碼編譯,及利用代碼文件生成為更底層目標文件的過程。在編譯型語言中,翻譯為機器碼等可被計算機直接執行的內容;在混合型語言中,則將代碼文件編譯為可被JVM等運行環境識別的內容。

由於用戶提供的僅為代碼部分,結合實際環境,代碼可能具備不可通用性,只在固定環境下才可通過編譯,由於用戶使用的不是IDE,關於代碼的診斷、編譯、加載過程都需要由平臺本身提供,即需要平臺開發者利用語言特性及固有工具開發相關功能。

關於編譯原理及流程相關內容,這裏不再贅述。

案例說明

網易貴金屬量化平臺使用的語言環境為Java。針對Java的編譯過程,在原生包javax.tools中提供了將Java源文件編譯為.class文件過程中需要的關鍵類,相關內容如下:

javax.tools.JavaCompiler:
/**
 * Interface to invoke Java™ programming language compilers from
 * programs.
 *
 * <p>The compiler might generate diagnostics during compilation (for
 * example, error messages).  If a diagnostic listener is provided,
 * the diagnostics will be supplied to the listener.  If no listener
 * is provided, the diagnostics will be formatted in an unspecified
 * format and written to the default output, which is {@code
 * System.err} unless otherwise specified.  Even if a diagnostic
 * listener is supplied, some diagnostics might not fit in a {@code
 * Diagnostic} and will be written to the default output.
 *
 ...

Java編譯工具, 編譯過程中會拋出相關的診斷信息。使用run方法執行編譯操作,也可先生成編譯任務(CompilationTask),之後調用CompilationTask的call方法執行編譯任務。

javax.tools.JavaFileObject:
/**
 * File abstraction for tools operating on Java&trade; programming language
 * source and class files.
 *
 * <p>All methods in this interface might throw a SecurityException if
 * a security exception occurs.
 *
 * <p>Unless explicitly allowed, all methods in this interface might
 * throw a NullPointerException if given a {@code null} argument.

Java源文件對象,負責源文件對象加載至內存。

javax.tools.JavaFileManage:
/**
 * File manager for tools operating on Java&trade; programming language
 * source and class files.  In this context, <em>file</em> means an
 * abstraction of regular files and other sources of data.
 ...

Java源文件管理類, 用於管理一系列JavaFileObject。

javax.tools.Diagnostic:
/**
 * Interface for diagnostics from tools.  A diagnostic usually reports
 * a problem at a specific position in a source file.  However, not
 * all diagnostics are associated with a position or a file.
 ...

Java文件診斷信息。

javax.tools.DiagnosticListener:
/**
 * Interface for receiving diagnostics from tools.
 *
 * @param <S> the type of source objects used by diagnostics received
 * by this listener
 *

診斷信息監聽器,編譯過程觸發。生成編譯任務(JavaCompiler.getTask())或獲取FileManager(JavaCompiler.getStandardFileManager())時需要傳遞DiagnosticListener以便收集診斷信息。

在以上相關類的基礎上,調用方式如下:

public static void compile(File srcFile, String targetClassPath) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> diagnosticListener = new DiagnosticCollector<>();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        Iterable it = fileManager.getJavaFileObjects(srcFile);
        createClassPathIfNotExists(targetClassPath);
        List<String> options = new ArrayList<>();
        options.add("-classpath");
        StringBuilder sb = new StringBuilder();
        URLClassLoader urlClassLoader = (URLClassLoader) Thread.currentThread().getContextClassLoader();        for (URL url : urlClassLoader.getURLs()) {
            sb.append(url.getFile().replace("%20", " ")).append(File.pathSeparator);
        }
        options.add(sb.toString());
        options.add("-d");
        options.add(targetClassPath);        try {
            JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnosticListener, options, null,
                    it);            boolean success = task.call();            if (!success) {
                StringBuilder errorMsg = new StringBuilder();                for (Diagnostic diagnostic : diagnosticListener.getDiagnostics()) {
                    errorMsg.append("line:").append(diagnostic.getLineNumber() - StrategyCodeConstant.DEFAULT_PRE_LINE)
                            .append(", ").append(diagnostic.getMessage(null)).append("\n");
                }                throw new CompileException(RetCode.COMPILE_ERROR, errorMsg.toString());
            }
        } catch (CompileException e) {            throw e;
        } catch (Exception e) {            throw new CompileException(RetCode.COMPILE_ERROR, e.getMessage(), e);
        }
    }

結合前面的方法說明,解釋方法內基本流程如下:

  1. 獲取系統編譯器

  2. 創建診斷監聽器

  3. 讀入Java源文件

  4. 創建目標class文件

  5. 設置類路徑等編譯參數

  6. 執行編譯任務

  7. 拋出診斷信息

經過上述流程後,如果監聽器未監聽到任何診斷,則最終生成的class文件可直接被類加載器加載並運行。

在class文件的留存方式上,可結合具體需要指定具體策略。如無需留存用戶代碼,則可采用二進制方式直接生成class文件對應的內存,如果需要留存用戶代碼,則看將編譯生成的class文件以其他方式進行轉存。

代碼運行

代碼運行,即將編譯後的內容加載至指定環境運行,各語言根據自身特性均會提供相關流程,本身並無難度。此處的代碼運行討論的內容,是如何將用戶代碼與在線編譯器平臺本身運行環境相結合。

  • 一般場景下,用戶代碼只依賴原生工具,自稱一體,如果語言存在類似JVM的運行環境,直接可以利用運行環境搭建簡易沙箱即可運行。

  • 特殊場景下,用戶代碼除必要原生工具外,對平臺本身提供的API強依賴,由於API內容包羅萬象,可能涉及到外部訪問或公共服務器內存使用,故單純的搭建沙箱,可能在一定程度上不能滿足需求。

既然單純的沙箱不能滿足需求,可能就面臨將用戶代碼加載至平臺所在的運行環境中一同運行的情況。但在這一過程中,如何規範用戶代碼的接入及調用動作就是重中之重,另外,如何在滿足用戶代碼運行基本需求的基礎上又能維護平臺安全就是必須解決的問題(ps:安全問題會在另一篇文章中進行闡述)。

規範用戶代碼的接入及調用動作,解決問題的入手點可以從以下幾個方面入手:

  • 明確用戶代碼調用內容:用戶代碼中究竟有何內容是必須使用平臺提供API的,是否可以窮舉所有行為。

  • 明確用戶代碼結構:在明確行為的基礎上,用戶代碼結構是否是可預知的,如果是可預知的,是否明確用戶代碼存在對外交互接口。

  • 明確用戶代碼調用方式:用戶代碼只需被調用一次,還是需要調用若幹次。

  • 明確用戶代碼可能出現的問題:即便是代碼診斷後,用戶代碼還是可能出現運行時異常,對於這些可能出現的運行時異常,要有預估以及處理方案,是否跳過本次執行或者打斷執行過程。

結合以上思考點,需要確定的內容是:平臺應該如何去調用用戶代碼,如何打通用戶代碼到平臺的壁壘。

在較為合理的情況下,用戶代碼經歷運行前的所有流程後,到這裏應該是可以預估形態的,用戶寫了什麽,會做什麽,怎麽用,平臺怎麽調用已經變得很明確了。

案例說明

網易貴金屬量化平臺,對用戶的策略代碼,直接規定了模板,用戶編寫的Java類必須繼承策略模板結構並實現相關方法。

類策略模板接口內容:

/**
 * 策略類
 */public interface Strategy {    /**
     * 策略初始化的時候調用一次,用於選擇品種,設置手續費,金額,等等
     * 
     * @param context 上下文
     */
    void init(Context context);    /**
     * 策略的主要實現
     * 
     * @param context 上下文
     */
    void handle(Context context);    /**
     * 策略運行結束時調用一次
     * 
     * @param context 上下文
     */
    void onExit(Context context);

}

用戶的策略代碼需要隨著時間推移,多次調用執行,進而模擬實際交易,此多次調用過程成為調度。具體的調用流程分為三個部分:

  1. 調度前:調用init方法,此方法內用戶需要初始化一些調度使用到的參數並給出初始值

  2. 調度中:按時間軸或行情消息驅動方式,不斷執行handle方法內的策略主要實現內容,過程中會更新handle方法內涉及到的變量內容,用戶可以在這一過程中可隨意使用對象內變量用於變量的臨時存儲。調度內容包括行情查詢、模擬開倉平倉操作、數學計算等

  3. 調度後:執行onExit方法,此方法內用戶可以結合自身需要做策略調度完成時的處理動作,可以進行自定義的統計計算或輸出日誌等

量化平臺通過規定用戶代碼結構的方式,進而規範了用戶代碼的調用方式,使所有的用戶代碼在調用過程中的行為保持統一。

內容反饋

內容反饋,用戶代碼由產生到運行,需讓用戶感知到代碼所產生的效果。在一般使用場景下,即簡單的在線編譯器,內容反饋表現在代碼的編譯情況以及代碼內輸出到控制臺的內容;但在特殊場景下,這兩部分的反饋內容對於用戶而言,是遠遠不夠的。

從反饋產生的時間上劃分,可分為以下三個階段:

  • 正式運行前,涵蓋內容包括:

    • 用戶代碼語法檢測情況

    • 用戶代碼編譯診斷情況

    • 用戶代碼環境加載情況

  • 正式運行時,涵蓋內容包括:

    • 用戶代碼運行中間值

    • 用戶代碼運行時異常及錯誤

    • 用戶代碼運行日誌

  • 運行結束後,涵蓋內容包括:

    • 用戶代碼調用結束通知

    • 用戶代碼方法返回值

    • 用戶代碼生成的數據

    • 用戶數據計算、統計、圖形化處理結果等

對於以上內容,平臺可選擇性的向用戶進行反饋。反饋方式上,可根據平臺的具體表現形式進行選擇,也可分為同步和異步兩部分進行分別通知,反饋通知形式包含:

  • 同步通知

    • 輸出控制臺

    • 消息窗體等及時推送與反饋

    • 日誌消息

  • 異步通知

    • 運行日誌文件

    • 運行情況報告文件

    • 用戶原始數據

    • 用戶數據計算、統計、圖形化處理結果

案例說明

網易貴金屬量化平臺,對於內容反饋部分的實現,分為以下幾個部分:

  1. 代碼檢測、診斷時,反饋檢測及診斷內容:

技術分享圖片

  1. 代碼運行時,反饋系統及用戶自定義日誌

技術分享圖片

  1. 代碼運行後,反饋策略日誌文件、原始交易信息、統計匯總

日誌文件

技術分享圖片

原始交易信息

技術分享圖片

統計匯總

技術分享圖片

反饋內容,應該結合需求用戶需求及使用反饋做出叠代調整,但內容只應限於用戶代碼涉及部分,不應透露服務器及平臺本身的運行狀態及重要參數信息。

後文鏈接

本文結合網易貴金屬量化平臺實際運用場景,闡述了在線編譯器搭建思路,分析了各類可能的應用場景及思考要點,在這一過程中詳細介紹了編輯及編譯的過程。關於用戶代碼的安全檢測與安全運行保障於後文闡述:

代碼在線編譯器(下)- 用戶代碼安全檢測

其他

Tool在線工具

推薦一個功能較為齊全的在線工具平臺:http://tool.lu/

Markdown圖片插入

由於Markdown不能直接插入圖片,圖片插入以鏈接方式進行,故需要用三方的圖床以存儲圖片並生成鏈接。推薦一個好用的圖床:微博圖床。可從chrome應用商店中下載插件,登錄微博後即可使用。可生成縮略圖及原圖的HTTP、HTML、UBB、MarkDown鏈接。


免費體驗雲安全(易盾)內容安全、驗證碼等服務

更多網易技術、產品、運營經驗分享請點擊。


相關文章:
【推薦】 Openwrt自定義CGI實現
【推薦】 構建SpringBoot基本框架(下篇)
【推薦】 考拉定時任務框架kSchedule

代碼在線編譯器(上)- 編輯及編譯