2012年5月30日

劍走偏鋒的Native Client

不知不覺,Google已經正式推出其Native Client (NaCl)過去約7個月之久。而目前國內似乎還沒有多少關於NaCl的資料,所以在這裡面向Web開發者做一下簡單的介紹,希望能夠起到一個拋磚引玉的效果。

本文的所有代碼均來自於https://developers.google.com/native-client/devguide/tutorial,如果您對其中的任何技術細節存在疑問,請以原文為準。

何謂NaCl

NaCl是一項能讓C/C++代碼運行在瀏覽器當中的技術。這是一個最通俗的說法,但不夠精確。嚴格來說,NaCl技術在理論上能夠實現任何編譯型語言都在其之上運行。

但是目前由於技術上的原因,NaCl還做不到任何語言任何平台的編譯型語言支持。

因為NaCl所憧憬的實際是LLVM技術,LLVM技術的要點即在於能將編譯型語言轉化為一個統一的中間語言,NaCl通過對這個中間語言的執行,即可達成任何編譯型語言的運行。換句話說,NaCl實際上希望搭建一個虛擬機。

不過LLVM現在還不夠成熟,NaCl不得不先使用GNU的編譯套件,使用LLVM技術的版本被稱為了PNaCl,目前還沒有正式推出。同時也因為這個原因,ARM架構沒能正式支持。

為什麼NaCl

在開發層面上,NaCl希望解決一個問題:JavaScript的低效率。當然,從經營策略上來說,Google可能還希望籍此將桌面領域的成熟軟件快速移植到其Chrome OS當中,不過這不是我們討論的重點。

JavaScript畢竟是一門解釋型語言,只有當瀏覽器執行到代碼的時刻才能夠看到代碼,因此在執行優化上力度非常小。

但是Web應用發展至今,效率已經必須拿到桌面上考慮,如果你還想在瀏覽器裡面看到更多優質的遊戲的話。

有關限制
  • 缺乏能夠切合的IDE
  • 不支持硬件異常
  • 不支持創建子進程
  • 不支持原生TCP/UDP操作(但已提供了websocket支持)
  • 不支持同步I/O
  • 不支持內存剩餘查詢
  • 內聯彙編代碼必須通過NaCl驗證
  • NaCl的Pepper API必須通過主線程使用

如何使用

上面那些對於行動主義來說其實都是P,真正需要聚焦的還是如何使用。

NaCl的典型項目由三個部分組成:
網頁(*.html)。這裡所指的網頁是一個泛指,它包括JS代碼、CSS樣式表已經HTML代碼。
NaCl模塊(*.c;*.cc)。這是C/C++代碼的文件。
清單(*.nmf)。這份清單類似於Chrome Extension的清單,主要用於指明在不同架構的機器上調用什麼模塊。

在真正開始之前,我們還需要安裝一個NaCl的SDK。這個SDK當中主要包含了NaCl的模塊編譯工具鏈。大家可以從這裡下載:https://developers.google.com/native-client/sdk/download

安裝之前,確保一下機器上有一個可用的Python 2.7,並加入到環境變量當中。

而SDK的安裝則相當簡單,只需要使用naclsdk update命令即可。

下面,創建一個名為hello_tutorial的目錄,我們來搭建一個簡單的demo。

網頁
 <script type="text/javascript">
    hello_tutorialModule = null;  // Global application object.
    statusText = 'NO-STATUS';

    // Indicate load success.
    function moduleDidLoad() {
      hello_tutorialModule = document.getElementById('hello_tutorial');
      updateStatus('SUCCESS');
    }

    // The 'message' event handler.  This handler is fired when the NaCl module
    // posts a message to the browser by calling PPB_Messaging.PostMessage()
    // (in C) or pp::Instance.PostMessage() (in C++).  This implementation
    // simply displays the content of the message in an alert panel.
    function handleMessage(message_event) {
      alert(message_event.data);
    }

    // If the page loads before the Native Client module loads, then set the
    // status message indicating that the module is still loading.  Otherwise,
    // do not change the status message.
    function pageDidLoad() {
      if (hello_tutorialModule == null) {
        updateStatus('LOADING...');
      } else {
        // It's possible that the Native Client module onload event fired
        // before the page's onload event.  In this case, the status message
        // will reflect 'SUCCESS', but won't be displayed.  This call will
        // display the current message.
        updateStatus();
      }
    }

    // Set the global status message.  If the element with id 'statusField'
    // exists, then set its HTML to the status message as well.
    // opt_message The message test.  If this is null or undefined, then
    // attempt to set the element with id 'statusField' to the value of
    // |statusText|.
    function updateStatus(opt_message) {
      if (opt_message)
        statusText = opt_message;
      var statusField = document.getElementById('status_field');
      if (statusField) {
        statusField.innerHTML = statusText;
      }
    }
  </script>
</head>
<body onload="pageDidLoad()">
<h1>Native Client Module hello_tutorial</h1>
<p>
  <!-- Load the published .nexe.  This includes the 'nacl' attribute which
  shows how to load multi-architecture modules.  Each entry in the "nexes"
  object in the .nmf manifest file is a key-value pair: the key is the
  instruction set architecture ('x86-32', 'x86-64', etc.); the value is a URL
  for the desired NaCl module.
  To load the debug versions of your .nexes, set the 'nacl' attribute to the
  _dbg.nmf version of the manifest file.

  Note: Since this NaCl module does not use any real-estate in the browser,
  it's width and height are set to 0.

  Note: The <EMBED> element is wrapped inside a <DIV>, which has both a 'load'
  and a 'message' event listener attached.  This wrapping method is used
  instead of attaching the event listeners directly to the <EMBED> element to
  ensure that the listeners are active before the NaCl module 'load' event
  fires.  This also allows you to use PPB_Messaging.PostMessage() (in C) or
  pp::Instance.PostMessage() (in C++) from within the initialization code in
  your NaCl module.
  -->
  <div id="listener">
    <script type="text/javascript">
      var listener = document.getElementById('listener');
      listener.addEventListener('load', moduleDidLoad, true);
      listener.addEventListener('message', handleMessage, true);
    </script>

    <embed name="nacl_module"
       id="hello_tutorial"
       width=0 height=0
       src="hello_tutorial.nmf"
       type="application/x-nacl" />
  </div>
</p>
<h2>Status</h2>
<div id="status_field">NO-STATUS</div>
</body>
</html>

NaCl模塊
/// @file hello_tutorial.cc
/// This example demonstrates loading, running and scripting a very simple NaCl
/// module.  To load the NaCl module, the browser first looks for the
/// CreateModule() factory method (at the end of this file).  It calls
/// CreateModule() once to load the module code from your .nexe.  After the
/// .nexe code is loaded, CreateModule() is not called again.
///
/// Once the .nexe code is loaded, the browser than calls the CreateInstance()
/// method on the object returned by CreateModule().  It calls CreateInstance()
/// each time it encounters an <embed> tag that references your NaCl module.
///
/// The browser can talk to your NaCl module via the postMessage() Javascript
/// function.  When you call postMessage() on your NaCl module from the browser,
/// this becomes a call to the HandleMessage() method of your pp::Instance
/// subclass.  You can send messages back to the browser by calling the
/// PostMessage() method on your pp::Instance.  Note that these two methods
/// (postMessage() in Javascript and PostMessage() in C++) are asynchronous.
/// This means they return immediately - there is no waiting for the message
/// to be handled.  This has implications in your program design, particularly
/// when mutating property values that are exposed to both the browser and the
/// NaCl module.
#include <cstdio>
#include <string>
#include "ppapi/cpp/instance.h"
#include "ppapi/cpp/module.h"
#include "ppapi/cpp/var.h"
/// The Instance class.  One of these exists for each instance of your NaCl
/// module on the web page.  The browser will ask the Module object to create
/// a new Instance for each occurence of the <embed> tag that has these
/// attributes:
///     type="application/x-nacl"
///     src="hello_tutorial.nmf"
/// To communicate with the browser, you must override HandleMessage() for
/// receiving messages from the borwser, and use PostMessage() to send messages
/// back to the browser.  Note that this interface is entirely asynchronous.
class hello_tutorialInstance : public pp::Instance {
 public:
  /// The constructor creates the plugin-side instance.
  /// @param[in] instance the handle to the browser-side plugin instance.
  explicit hello_tutorialInstance(PP_Instance instance) : pp::Instance(instance)
  {}
  virtual ~hello_tutorialInstance() {}

  /// Handler for messages coming in from the browser via postMessage().  The
  /// @a var_message can contain anything: a JSON string; a string that encodes
  /// method names and arguments; etc.  For example, you could use
  /// JSON.stringify in the browser to create a message that contains a method
  /// name and some parameters, something like this:
  ///   var json_message = JSON.stringify({ "myMethod" : "3.14159" });
  ///   nacl_module.postMessage(json_message);
  /// On receipt of this message in @a var_message, you could parse the JSON to
  /// retrieve the method name, match it to a function call, and then call it
  /// with the parameter.
  /// @param[in] var_message The message posted by the browser.
  virtual void HandleMessage(const pp::Var& var_message) {
    // TODO(sdk_user): 1. Make this function handle the incoming message.
  }
};
/// The Module class.  The browser calls the CreateInstance() method to create
/// an instance of your NaCl module on the web page.  The browser creates a new
/// instance for each <embed> tag with type="application/x-nacl".
class hello_tutorialModule : public pp::Module {
 public:
  hello_tutorialModule() : pp::Module() {}
  virtual ~hello_tutorialModule() {}

  /// Create and return a hello_tutorialInstance object.
  /// @param[in] instance The browser-side instance.
  /// @return the plugin-side instance.
  virtual pp::Instance* CreateInstance(PP_Instance instance) {
    return new hello_tutorialInstance(instance);
  }
};
namespace pp {
/// Factory function called by the browser when the module is first loaded.
/// The browser keeps a singleton of this module.  It calls the
/// CreateInstance() method on the object you return to make instances.  There
/// is one instance per <embed> tag on the page.  This is the main binding
/// point for your NaCl module with the browser.
Module* CreateModule() {
  return new hello_tutorialModule();
}
}  // namespace pp

Makefile

Makefile是C\C++編譯指令的存放文件,這份文件將指引編譯器、鏈接器如何工作。因為我們現在所處情況特殊,所以Makefile需要自己編寫。 
# Copyright (c) 2012 The Native Client Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# GNU Make based build file.  For details on GNU Make see:
# http://www.gnu.org/software/make/manual/make.html
#
#
# Project information
#
# These variables store project specific settings for the project name
# build flags, files to copy or install. In the examples it is typically
# only the list of sources and project name that will actually change and
# the rest of the makefile is boilerplate for defining build rules.
#
PROJECT:=hello_tutorial
LDFLAGS:=-lppapi_cpp -lppapi
CXX_SOURCES:=$(PROJECT).cc

#
# Get pepper directory for toolchain and includes.
#
# If NACL_SDK_ROOT is not set, then assume it can be found a two directories up,
# from the default example directory location.
#
THIS_MAKEFILE:=$(abspath $(lastword $(MAKEFILE_LIST)))
NACL_SDK_ROOT?=$(abspath $(dir $(THIS_MAKEFILE))../..)
# Project Build flags
WARNINGS:=-Wno-long-long -Wall -Wswitch-enum -pedantic -Werror
CXXFLAGS:=-pthread -std=gnu++98 $(WARNINGS)
#
# Compute tool paths
#
#
OSNAME:=$(shell python $(NACL_SDK_ROOT)/tools/getos.py)
TC_PATH:=$(abspath $(NACL_SDK_ROOT)/toolchain/$(OSNAME)_x86_newlib)
CXX:=$(TC_PATH)/bin/i686-nacl-g++
#
# Disable DOS PATH warning when using Cygwin based tools Windows
#
CYGWIN ?= nodosfilewarningexport CYGWIN

# Declare the ALL target first, to make the 'all' target the default build
all: $(PROJECT)_x86_32.nexe $(PROJECT)_x86_64.nexe
# Define 32 bit compile and link rules for main application
x86_32_OBJS:=$(patsubst %.cc,%_32.o,$(CXX_SOURCES))
$(x86_32_OBJS) : %_32.o : %.cc $(THIS_MAKE)
        $(CXX) -o $@ -c $< -m32 -O0 -g $(CXXFLAGS)

$(PROJECT)_x86_32.nexe : $(x86_32_OBJS)
        $(CXX) -o $@ $^ -m32 -O0 -g $(CXXFLAGS) $(LDFLAGS)
# Define 64 bit compile and link rules for C++ sources
x86_64_OBJS:=$(patsubst %.cc,%_64.o,$(CXX_SOURCES))
$(x86_64_OBJS) : %_64.o : %.cc $(THIS_MAKE)
        $(CXX) -o $@ -c $< -m64 -O0 -g $(CXXFLAGS)

$(PROJECT)_x86_64.nexe : $(x86_64_OBJS)
        $(CXX) -o $@ $^ -m64 -O0 -g $(CXXFLAGS) $(LDFLAGS)
# Define a phony rule so it always runs, to build nexe and start up server.
.PHONY: RUN 
RUN: all
  python ../httpd.py

清單文件

清單中指出了不同架構所使用的模塊。
{
  "program": {
    "x86-64": {"url": "hello_tutorial_x86_64.nexe"},
    "x86-32": {"url": "hello_tutorial_x86_32.nexe"}
  }
}

所有這些文件齊全之後,只需要make即可完成自動編譯。

如何運行

在編譯之後,雙擊html頁面打開其實並不會載入模塊運行,這是由於瀏覽器的訪問域規則不允許直接在用戶的本地讀取文件。因此,我們需要讓本機成為一個服務器,以遠程服務器的身份來讀取模塊最終傳送給瀏覽器。

這很容易,SDK當中已經包含了提供http服務的python腳本:
cd pepper_18/examples/hello_tutorial
make

這樣,http://localhost:5103就成為了我們的服務器地址。注意,http服務的根目錄的位置位於examples目錄。

現在,我們在瀏覽器的chrome://flags和chrome://plugins頁面中打開NaCl的幾個相關選項,就可以成功運行了。

Via NewHTML

沒有留言:

張貼留言

Google 發布第一段Google 眼鏡的使用說明視頻

Google今天發布了一段叫做Glass How-to: Getting Started的視頻,入門性的介紹了Google眼鏡的使用方式,這應該是眾多視頻中的一個,接下來估計還會有更深入的視頻吧? 這段視頻介紹了你如何觀看到棱鏡屏幕,使用右側眼鏡腿兒觸摸區域操作眼鏡的基本...