tag:blogger.com,1999:blog-10464189830813731672024-03-13T13:12:51.096+09:00Wander Alone Like A Rhinoceros Horn久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.comBlogger40125tag:blogger.com,1999:blog-1046418983081373167.post-80037380633256314672010-11-01T10:46:00.000+09:002017-05-04T08:00:29.228+09:00Wander Alone Like A Rhinoceros Horn多忙な過去2年間が過ぎ、荷を降ろしてみると、見過ごしていた(あるいは故意に目を逸らしていた?)様々な事柄が眼に飛び込んでくる。そして、2年の月日は、諸々の事象を眺める私の眼そのものをも変容させていた。変容は、フィジカル、メタフィジカル、両面に及んでいる。回復された視力、衰えた肉体、削ぎ落とされた関心、獲得した視座。<br />
<br />
過去に書いた文章を読み直すと、自分の変化と世界の変化が響き合うとともに軋み合い、交錯に眩暈を覚える。たとえば4年前に<a href="http://ryukbk.blogspot.jp/2006/11/ajax.html" target="_blank">AJAXやFlashを題材にして書いた記事</a>では、Webページがアプリになる可能性を考察していた。その可能性の一端は現在HTML5として実現している。ただし、HTML5には技術的に新しいことは何もなく、政治的理由により遅れてやってきた標準化にすぎない。また、同じく2006年に、今回と同じく過去を回顧している<a href="http://ryukbk.blogspot.jp/2006/11/shonuff.html" target="_blank">文章</a>では、「無駄な部分を省いたコミュニケーションの需要は残るはずであり、むしろ参入障壁が十分に低ければシンプルなものがリッチなものを出し抜く可能性が十分にある」と書いており、こちらはTwitterとして顕在化してきた。しかし2006年の時点では、巨大なデータを合成してシンプルなインターフェイスに落とし込む処理を、自分では全くイメージできなかった。<br />
<br />
8bit時代のビデオゲームは、表現能力を欠いたことにより洋の東西を問わない普遍性を獲得した。21世紀的なシンプルさの形式が普遍性を備えるには、世界中のユーザーの要求に即座に応答できるシステムを背後に組織化しなければならない。スマートフォン、クラウド... ハードウェアにいくつセンサーやプロセッサーが導入されようが、ソフトウェアデザインは本質的に自由だ。その広すぎる可能性の中から、有限の資源に応じた、核心を衝く着想が常に求められている。<br />
<br />
この2年間は、専らWebブラウザーの発展に注目してきた。Jargon Fileにも載っている用語で<a href="http://www.catb.org/jargon/html/W/wheel-of-reincarnation.html" target="_blank">wheel of reincarnation</a>(輪廻)というものがある。ある処理に特化したプロセッサ(たとえばGPU)をCPUから分離した場合に、後年そのプロセッサの汎用計算性能が強化されるにつれ結局はCPU上でソフトウェア実装した方が効率的になり、再度CPUに吸収される...というサイクルが反復されることを指して輪廻と呼ぶ。Webブラウザーも、PCの本質的な機能であるということになれば、<a href="http://ryukbk.blogspot.jp/2009/08/red-pill-or-blue-pill.html" target="_blank">OSに統合されるのが望ましい</a>。ところが、政治的事情からWindowsではそうはなっていない。iOSやAndroid、Chrome OSでは統合されているというのにWindowsではそうはなっていないというのは笑うしかない。どちらが望ましいのだろうか。一つだけ明らかなのは、私は自由なソフトウェアを支持するということである。つまり、iOSの現状が、競争を排除するようなものであるとすれば、私はそれを忌避するし、Apple社の製品は購入しない。私にとってのiPhoneとは、Androidの健全な発展を促す触媒でしかない。<br />
<br />
Webブラウザーに関して、直近の動きで一番注目しているのは<a href="http://mozillalabs.com/chromeless/2010/10/21/chromeless-build-your-own-browser-ui-using-html-css-js/" target="_blank">Chromeless</a>だ。XULがHTMLに溶け出している。自分が欲しかったコンセプトがそのままプロジェクトになっていて、嬉しかった。他に興味深かったのは、Beyond3Dで<a href="http://forum.beyond3d.com/showpost.php?p=1483011&postcount=155" target="_blank">教えてもらった</a>、EAによるPS3/Xbox360への<a href="http://gpl.ea.com/skate3.html" target="_blank">WebKit移植</a>のベースに利用されていた<a href="http://en.wikipedia.org/wiki/Origyn_Web_Browser" target="_blank">Origyn Web Browser</a>。現在サイトが落ちているようなのでEA側のコードしか見ることは出来ない。EA側コードは、Paul Pedriana氏による<a href="https://github.com/paulhodge/EASTL" target="_blank">EASTL</a>実装を含む(ドキュメントには<a href="http://beta.boost.org/doc/libs/1_42_0/doc/html/intrusive.html" target="_blank">Boost.Intrusive</a>みたいなコンテナーが入っていると書かれているが見つからなかった)。<br />
<br />
次に向けて、学ばなければならないことは山ほどある。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.comtag:blogger.com,1999:blog-1046418983081373167.post-88526457094123287682010-10-12T05:19:00.000+09:002017-05-31T19:59:59.209+09:00Dissecting WebKit2 Implementation as of October 2010<a href="http://trac.webkit.org/wiki/WebKit2" target="_blank">WebKit2</a> was first disclosed to the public in April 2010. Now half a year has passed, has there been any noticeable progress? This time I delve into the WebKit2 from the source-code point of view.<br />
<br />
First let me recap what WebKit2 actually is and how it's related to the current WebKit with the <a href="http://trac.webkit.org/wiki/WebKit2" target="_blank">official high level design document</a> as the reference.<br />
<br />
For those who are not very familiar with WebKit, it may be confusing that WebKit as a whole contains a part that shares the same <em>WebKit</em> name. WebKit as the framework has 3 major components - <a href="http://trac.webkit.org/browser/trunk/WebKit" target="_blank">WebKit</a>, <a href="http://trac.webkit.org/browser/trunk/WebCore" target="_blank">WebCore</a>, and <a href="http://trac.webkit.org/browser/trunk/JavaScriptCore" target="_blank">JavaScriptCore</a> - as you see in the <a href="http://trac.webkit.org/browser/trunk" target="_blank">source directory</a>. WebCore is the layout engine that parses an HTML5 web page into a DOM tree and renders a render tree created out of a DOM tree with CSS information computed on each node. In addition to the abstract or platform-agnostic parts, it contains platform-specific implementations such as graphics and I/O if necessary. JavaScriptCore is the JavaScript engine. Google Chrome swaps it with its own <a href="http://code.google.com/p/v8/" target="_blank">V8</a> engine whereas Safari uses JavaScriptCore under the name Nitro. JavaScriptCore/V8 bindings to DOM live in the <a href="http://trac.webkit.org/browser/trunk/WebCore/bindings" target="_blank">bindings directory</a> in the WebCore. A WebKit user controls WebCore through a high-level API set which is differently implemented for each platform. The WebKit directory contains such an API set. For example, the Windows version of Safari implements it by COM classes in C++ in the <a href="http://trac.webkit.org/browser/trunk/WebKit/win" target="_blank">win</a> directory while the Mac version does it with Objective-C in the <a href="http://trac.webkit.org/browser/trunk/WebKit/mac" target="_blank">mac</a> directory. Due to some dependency on proprietary libraries owned by Apple found in the Windows implementation for Safari, WebKit embedders should try other API layer implementations such as Gtk+ or Qt unless adopting <a href="http://code.google.com/chromium/" target="_blank">Chromium</a> suits the need.<br />
<br />
<a name='more'></a><br />
WebKit2 effectively splits this WebKit API layer into two. The first part, UI Process, is obviously the interface accessed by the platform API user. Inside this shell, or outside if you follow its process model, there are Web Processes that implement the second layer of the new WebKit2 API. The UI Process controls multiple Web Processes through accessing the API based on an IPC protocol agreed by them. The API of the UI Process should work in almost the same way for a user as the old API except that the WebKit2 API are non-blocking. Most likely the underlying WebCore doesn't change much as a multiprocess architecture has typically a lot less synchronization issues than a multithreaded architecture. Also the current WebKit API should continue to work alongside of the WebKit2 development unless Chromium tries to merge it, but the prospect looks unlikely at this point.<br />
<br />
Let's take a look at the source code itself. The most easiest entry point would be an example that uses WebKit2 API. Fortunately it's already available as the <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser" target="_blank">MiniBrowser</a>. Right now there are 3 implementations: Mac, Windows, and Qt. The Mac version seems the most active, the next is the Qt version. The Windows version is a plain Windows application that is not related to the current Windows version of Safari for some reason. Since it's the most simple, I start from there. The <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser/win/main.cpp" target="_blank">main.cpp</a> has its message loop.<br />
<br />
<pre class="brush: cpp">int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpstrCmdLine, int nCmdShow)
{
MiniBrowser::shared().initialize(hInstance);
// Create and show our initial window.
MiniBrowser::shared().createNewWindow();
MSG message;
while (BOOL result = ::GetMessage(&message, 0, 0, 0)) {
if (result == -1)
break;
::TranslateMessage(&message);
if (!MiniBrowser::shared().handleMessage(&message))
::DispatchMessage(&message);
}
return 0;
}
</pre>
<br />
<br />
<code>MiniBrowser::shared()</code> returns the static singleton of the <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser/win/MiniBrowser.h" target="_blank">MiniBrowser</a> object. So it calls <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser/win/MiniBrowser.cpp" target="_blank"><code>MiniBrowser::createNewWindow()</code></a> which then creates a <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser/win/BrowserWindow.h" target="_blank">BrowserWindow</a> object with its member BrowserView object (<code>m_browserView</code>). The actual browser view is created at the end of <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser/win/BrowserWindow.cpp" target="_blank"><code>BrowserWindow::onCreate</code></a>. It calls <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser/win/BrowserView.cpp" target="_blank"><code>BrowserView::create</code></a>.<br />
<br />
<pre class="brush: cpp">void BrowserView::create(RECT webViewRect, BrowserWindow* parentWindow)
{
assert(!m_webView);
bool isShiftKeyDown = ::GetKeyState(VK_SHIFT) & HIGH_BIT_MASK_SHORT;
WKContextRef context;
if (isShiftKeyDown)
context = WKContextGetSharedThreadContext();
else
context = WKContextGetSharedProcessContext();
WKPageNamespaceRef pageNamespace = WKPageNamespaceCreate(context);
m_webView = WKViewCreate(webViewRect, pageNamespace, parentWindow->window());
WKPageUIClient uiClient = {
0, /* version */
parentWindow, /* clientInfo */
createNewPage,
showPage,
closePage,
runJavaScriptAlert,
runJavaScriptConfirm,
runJavaScriptPrompt,
setStatusText,
mouseDidMoveOverElement,
0 /* didNotHandleKeyEvent */
};
WKPageSetPageUIClient(WKViewGetPage(m_webView), &uiClient);
}
</pre>
<br />
<br />
A lot of things seem to happen, but apparently this is the starting point of embedding WebKit2. <code>isShiftKeyDown</code> is probably there for the debug purpose, it's used to switch the context. <code>WKContextGetSharedThreadContext</code> is declared in <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/API/C/WKContextPrivate.h" target="_blank">WKContextPrivate.h</a> while <code>WKContextGetSharedProcessContext</code> is declared in <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/API/C/WKContext.h" target="_blank">WKContext.h</a>, both are WebKit2 C API functions. These actually refer to the <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebContext.cpp" target="_blank"><code>WebContext</code></a> object of the UI Process.<br />
<br />
<pre class="brush: cpp">WebContext* WebContext::sharedProcessContext()
{
WTF::initializeMainThread();
RunLoop::initializeMainRunLoop();
static WebContext* context = adoptRef(new WebContext(ProcessModelSharedSecondaryProcess, String())).leakRef();
return context;
}
WebContext* WebContext::sharedThreadContext()
{
RunLoop::initializeMainRunLoop();
static WebContext* context = adoptRef(new WebContext(ProcessModelSharedSecondaryThread, String())).leakRef();
return context;
}
</pre>
<br />
<br />
This <code>RunLoop</code> is declared in <a href="http://trac.webkit.org/browser/trunk/WebKit2/Platform/RunLoop.h" target="_blank">RunLoop.h</a>,but most of its work is defined in the platform-specific implementation, for example the Windows version can be found in <a href="http://trac.webkit.org/browser/trunk/WebKit2/Platform/win/RunLoopWin.cpp" target="_blank">RunLoopWin.cpp</a>. It's a message queue window for WebKit work items. So, back to the <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser/win/BrowserView.cpp" target="_blank"><code>BrowserView::create</code></a>, it creates a <code>WebContext</code> of some flavor. Its <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebContext.h" target="_blank">member declaration</a> is like this.<br />
<br />
<pre class="brush: cpp">class WebContext : public APIObject {
public:
static const Type APIType = TypeContext;
static WebContext* sharedProcessContext();
static WebContext* sharedThreadContext();
static PassRefPtr<WebContext> create(const String& injectedBundlePath);
~WebContext();
// snip
private:
WebContext(ProcessModel, const String& injectedBundlePath);
virtual Type type() const { return APIType; }
void ensureWebProcess();
bool hasValidProcess() const { return m_process && m_process->isValid(); }
void platformInitializeWebProcess(WebProcessCreationParameters&);
static void languageChanged(void* context);
void languageChanged();
ProcessModel m_processModel;
// FIXME: In the future, this should be one or more WebProcessProxies.
RefPtr<WebProcessProxy> m_process;
HashSet<WebPageNamespace*> m_pageNamespaces;
RefPtr<WebPreferences> m_preferences;
String m_injectedBundlePath;
WebContextInjectedBundleClient m_injectedBundleClient;
WebHistoryClient m_historyClient;
PluginInfoStore m_pluginInfoStore;
VisitedLinkProvider m_visitedLinkProvider;
HashSet<String> m_schemesToRegisterAsEmptyDocument;
HashSet<String> m_schemesToRegisterAsSecure;
HashSet<String> m_schemesToSetDomainRelaxationForbiddenFor;
Vector<pair<String, RefPtr<APIObject> > > m_pendingMessagesToPostToInjectedBundle;
CacheModel m_cacheModel;
#if PLATFORM(WIN)
bool m_shouldPaintNativeControls;
#endif
};
</pre>
<br />
<br />
It has document-related objects such as <code>WebHistoryClient</code> and <code>VisitedLinkProvider</code>, also it has <code>WebPreferences</code> that should be applied to all pages. So <code>WebContext</code> is the class that represents a single web browser.<br />
<br />
The other important member object is <code>WebProcessProxy</code> that is a proxy to a renderer process. As the comment suggests, it will be the proxy to multiple Web Processes in the future. The web process proxy is initialized before creating a web page by <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebProcessProxy.cpp" target="_blank"><code>WebContext::ensureWebProcess</code></a>.<br />
<br />
<pre class="brush: cpp">void WebContext::ensureWebProcess()
{
if (m_process)
return;
m_process = WebProcessManager::shared().getWebProcess(this);
WebProcessCreationParameters parameters;
parameters.applicationCacheDirectory = applicationCacheDirectory();
if (!injectedBundlePath().isEmpty()) {
parameters.injectedBundlePath = injectedBundlePath();
#if ENABLE(WEB_PROCESS_SANDBOX)
char* sandboxBundleTokenUTF8 = 0;
CString injectedBundlePathUTF8 = injectedBundlePath().utf8();
sandbox_issue_extension(injectedBundlePathUTF8.data(), &sandboxBundleTokenUTF8);
String sandboxBundleToken = String::fromUTF8(sandboxBundleTokenUTF8);
if (sandboxBundleTokenUTF8)
free(sandboxBundleTokenUTF8);
parameters.injectedBundlePathToken = sandboxBundleToken;
#endif
}
parameters.shouldTrackVisitedLinks = m_historyClient.shouldTrackVisitedLinks();
parameters.cacheModel = m_cacheModel;
parameters.languageCode = defaultLanguage();
parameters.applicationCacheDirectory = applicationCacheDirectory();
copyToVector(m_schemesToRegisterAsEmptyDocument, parameters.urlSchemesRegistererdAsEmptyDocument);
copyToVector(m_schemesToRegisterAsSecure, parameters.urlSchemesRegisteredAsSecure);
copyToVector(m_schemesToSetDomainRelaxationForbiddenFor, parameters.urlSchemesForWhichDomainRelaxationIsForbidden);
// Add any platform specific parameters
platformInitializeWebProcess(parameters);
m_process->send(Messages::WebProcess::InitializeWebProcess(parameters), 0);
}
</pre>
<br />
<br />
<a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebProcessManager.cpp" target="_blank"><code>WebProcessManager</code></a> does simple creation of a Web Process proxy via <code>WebProcessProxy::create</code> dependent on the process model: Shared Secondary Process, Shared Secondary Thread, Secondary Process. The names are a bit confusing but Shared Secondary Thread means using threads for Web Processes, and Shared Secondary Process means using multiple processes. The last one manages multiple WebContext objects and correspondent Web Proxy objects in a hash map.<br />
<br />
<pre class="brush: cpp">WebProcessProxy* WebProcessManager::getWebProcess(WebContext* context)
{
switch (context->processModel()) {
case ProcessModelSharedSecondaryProcess: {
if (!m_sharedProcess)
m_sharedProcess = WebProcessProxy::create(context);
return m_sharedProcess.get();
}
case ProcessModelSharedSecondaryThread: {
if (!m_sharedThread)
m_sharedThread = WebProcessProxy::create(context);
return m_sharedThread.get();
}
case ProcessModelSecondaryProcess: {
std::pair<ProcessMap::iterator, bool> result = m_processMap.add(context, 0);
if (result.second) {
ASSERT(!result.first->second);
result.first->second = WebProcessProxy::create(context);
}
ASSERT(result.first->second);
return result.first->second.get();
}
}
ASSERT_NOT_REACHED();
return 0;
}
</pre>
<br />
<br />
<code>WebProcessProxy::create</code> is just a call to <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebProcessProxy.cpp" target="_blank"><code>WebProcessProxy::connect</code></a> via its constructor.<br />
<br />
<pre class="brush: cpp">PassRefPtr<WebProcessProxy> WebProcessProxy::create(WebContext* context)
{
return adoptRef(new WebProcessProxy(context));
}
WebProcessProxy::WebProcessProxy(WebContext* context)
: m_responsivenessTimer(this)
, m_context(context)
{
connect();
}
WebProcessProxy::~WebProcessProxy()
{
ASSERT(!m_connection);
for (size_t i = 0; i < m_pendingMessages.size(); ++i)
m_pendingMessages[i].releaseArguments();
if (m_processLauncher) {
m_processLauncher->invalidate();
m_processLauncher = 0;
}
if (m_threadLauncher) {
m_threadLauncher->invalidate();
m_threadLauncher = 0;
}
}
void WebProcessProxy::connect()
{
if (m_context->processModel() == ProcessModelSharedSecondaryThread) {
ASSERT(!m_threadLauncher);
m_threadLauncher = ThreadLauncher::create(this);
} else {
ASSERT(!m_processLauncher);
ProcessLauncher::LaunchOptions launchOptions;
launchOptions.processType = ProcessLauncher::WebProcess;
#if PLATFORM(MAC)
// We want the web process to match the architecture of the UI process.
launchOptions.architecture = ProcessLauncher::LaunchOptions::MatchCurrentArchitecture;
#endif
m_processLauncher = ProcessLauncher::create(this, launchOptions);
}
}
</pre>
<br />
<br />
If the process model is Shared Secondary Thread, it creates a <code>ThreadLauncher</code> instance by its <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/Launcher/ThreadLauncher.h" target="_blank"><code>create</code></a> method.<br />
<br />
<pre class="brush: cpp">class ThreadLauncher : public ThreadSafeShared<ThreadLauncher> {
public:
class Client {
public:
virtual ~Client() { }
virtual void didFinishLaunching(ThreadLauncher*, CoreIPC::Connection::Identifier) = 0;
};
static PassRefPtr<ThreadLauncher> create(Client* client)
{
return adoptRef(new ThreadLauncher(client));
}
</pre>
<br />
<br />
Again, it's just a proxy to <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/Launcher/ThreadLauncher.cpp" target="_blank"><code>ThreadLauncher::launchThread</code></a>.<br />
<br />
<pre class="brush: cpp">ThreadLauncher::ThreadLauncher(Client* client)
: m_client(client)
{
launchThread();
}
void ThreadLauncher::launchThread()
{
m_isLaunching = true;
CoreIPC::Connection::Identifier connectionIdentifier = createWebThread();
// We've finished launching the thread, message back to the main run loop.
RunLoop::main()->scheduleWork(WorkItem::create(this, &ThreadLauncher::didFinishLaunchingThread, connectionIdentifier));
}
</pre>
<br />
<br />
ThreadLauncher::createWebThread is a platform-specific method. It's defined in <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/Launcher/win/ThreadLauncherWin.cpp" target="_blank">ThreadLauncherWin.cpp</a> for Windows.<br />
<br />
<pre class="brush: cpp">CoreIPC::Connection::Identifier ThreadLauncher::createWebThread()
{
// First, create the server and client identifiers.
HANDLE serverIdentifier, clientIdentifier;
if (!CoreIPC::Connection::createServerAndClientIdentifiers(serverIdentifier, clientIdentifier)) {
// FIXME: What should we do here?
ASSERT_NOT_REACHED();
}
if (!createThread(webThreadBody, reinterpret_cast<void*>(clientIdentifier), "WebKit2: WebThread")) {
::CloseHandle(serverIdentifier);
::CloseHandle(clientIdentifier);
return 0;
}
return serverIdentifier;
}
</pre>
<br />
<br />
<a href="http://trac.webkit.org/browser/trunk/JavaScriptCore/wtf/Threading.h" target="_blank"><code>createThread</code></a> is a utility function declared in the WTF namespace of JavaScriptCore. In <a href="http://trac.webkit.org/browser/trunk/JavaScriptCore/wtf/Threading.cpp" target="_blank"><code>wtf/Threading.cpp</code></a>, it calls platform-specific <code>createThreadInternal</code>.<br />
<br />
<pre class="brush: cpp">ThreadIdentifier createThread(ThreadFunction entryPoint, void* data, const char* name)
{
// Visual Studio has a 31-character limit on thread names. Longer names will
// be truncated silently, but we'd like callers to know about the limit.
#if !LOG_DISABLED
if (strlen(name) > 31)
LOG_ERROR("Thread name \"%s\" is longer than 31 characters and will be truncated by Visual Studio", name);
#endif
NewThreadContext* context = new NewThreadContext(entryPoint, data, name);
// Prevent the thread body from executing until we've established the thread identifier.
MutexLocker locker(context->creationMutex);
return createThreadInternal(threadEntryPoint, context, name);
}
</pre>
<br />
<br />
For example, the Windows version is defined in <a href="http://trac.webkit.org/browser/trunk/JavaScriptCore/wtf/ThreadingWin.cpp" target="_blank"><code>wtf/ThreadingWin.cpp</code></a> (for some reason this is not in a separate folder).<br />
<br />
<pre class="brush: cpp">ThreadIdentifier createThreadInternal(ThreadFunction entryPoint, void* data, const char* threadName)
{
unsigned threadIdentifier = 0;
ThreadIdentifier threadID = 0;
ThreadFunctionInvocation* invocation = new ThreadFunctionInvocation(entryPoint, data);
#if OS(WINCE)
// This is safe on WINCE, since CRT is in the core and innately multithreaded.
// On desktop Windows, need to use _beginthreadex (not available on WinCE) if using any CRT functions
HANDLE threadHandle = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)wtfThreadEntryPoint, invocation, 0, (LPDWORD)&threadIdentifier);
#else
HANDLE threadHandle = reinterpret_cast<HANDLE>(_beginthreadex(0, 0, wtfThreadEntryPoint, invocation, 0, &threadIdentifier));
#endif
if (!threadHandle) {
#if OS(WINCE)
LOG_ERROR("Failed to create thread at entry point %p with data %p: %ld", entryPoint, data, ::GetLastError());
#elif defined(NO_ERRNO)
LOG_ERROR("Failed to create thread at entry point %p with data %p.", entryPoint, data);
#else
LOG_ERROR("Failed to create thread at entry point %p with data %p: %ld", entryPoint, data, errno);
#endif
return 0;
}
threadID = static_cast<ThreadIdentifier>(threadIdentifier);
storeThreadHandleByIdentifier(threadIdentifier, threadHandle);
return threadID;
}
</pre>
<br />
<br />
<a href="http://msdn.microsoft.com/en-us/library/kdzttdcb.aspx" target="_blank"><code>_beginthreadex</code></a> is the underlying thread creation function on Windows. In this model, the thread ID is used as the return value which becomes CoreIPC::Connection::Identifier.<br />
<br />
You may remember the <a href="http://www.chromium.org/developers/design-documents/process-models#TOC-Single-process" target="_blank">single process model</a> in Chromium as a mode akin to this Shared Secondary Thread prcess model in WebKit2.<br />
<br />
OK let's get back to the more interesting one, the Shared Secondary Process model that sports <code>ProcessLauncher</code>. When created via the <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/Launcher/ProcessLauncher.h" target="_blank"><code>create</code></a> method, its <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/Launcher/ProcessLauncher.cpp" target="_blank">constructor</a> adds a function call to <code>ProcessLauncher::launchProcess</code> to the end of its <code>WorkQueue</code> singleton.<br />
<br />
<pre class="brush: cpp">class ProcessLauncher : public ThreadSafeShared<ProcessLauncher> {
public:
class Client {
public:
virtual ~Client() { }
virtual void didFinishLaunching(ProcessLauncher*, CoreIPC::Connection::Identifier) = 0;
};
// snip
static PassRefPtr<ProcessLauncher> create(Client* client, const LaunchOptions& launchOptions)
{
return adoptRef(new ProcessLauncher(client, launchOptions));
}
</pre>
<br />
<br />
<pre class="brush: cpp">static WorkQueue& processLauncherWorkQueue()
{
DEFINE_STATIC_LOCAL(WorkQueue, processLauncherWorkQueue, ("com.apple.WebKit.ProcessLauncher"));
return processLauncherWorkQueue;
}
ProcessLauncher::ProcessLauncher(Client* client, const LaunchOptions& launchOptions)
: m_client(client)
, m_launchOptions(launchOptions)
, m_processIdentifier(0)
{
// Launch the process.
m_isLaunching = true;
processLauncherWorkQueue().scheduleWork(WorkItem::create(this, &ProcessLauncher::launchProcess));
}
</pre>
<br />
<br />
<a href="http://trac.webkit.org/browser/trunk/WebKit2/Platform/win/WorkQueueWin.cpp" target="_blank"><code>WorkQueue::scheduleWork</code></a> is defined as a platform-specific function. In the case of Windows, it's based on the <a href="http://msdn.microsoft.com/en-us/library/ms684957.aspx" target="_blank"><code>QueueUserWorkItem</code></a> API that uses the thread pool of Windows OS. Again, <code>ProcessLauncher::launchProcess</code> is implemented in the platform-specific form. For Windows, it's in <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/Launcher/win/ProcessLauncherWin.cpp" target="_blank">win/ProcessLauncherWin.cpp</a>.<br />
<br />
<pre class="brush: cpp">void ProcessLauncher::launchProcess()
{
// First, create the server and client identifiers.
HANDLE serverIdentifier, clientIdentifier;
if (!CoreIPC::Connection::createServerAndClientIdentifiers(serverIdentifier, clientIdentifier)) {
// FIXME: What should we do here?
ASSERT_NOT_REACHED();
}
// Ensure that the child process inherits the client identifier.
::SetHandleInformation(clientIdentifier, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
// To get the full file path to WebKit2WebProcess.exe, we fild the location of WebKit.dll,
// remove the last path component, and then append WebKit2WebProcess(_debug).exe.
HMODULE webKitModule = ::GetModuleHandleW(webKitDLLName);
ASSERT(webKitModule);
if (!webKitModule)
return;
WCHAR pathStr[MAX_PATH];
if (!::GetModuleFileNameW(webKitModule, pathStr, WTF_ARRAY_LENGTH(pathStr)))
return;
::PathRemoveFileSpecW(pathStr);
if (!::PathAppendW(pathStr, webProcessName))
return;
String commandLine(pathStr);
// FIXME: It would be nice if we could just create a CommandLine object and output a command line vector from it.
Vector<UChar> commandLineVector;
append(commandLineVector, commandLine);
append(commandLineVector, " -type webprocess");
append(commandLineVector, " -clientIdentifier ");
append(commandLineVector, String::number(reinterpret_cast<uintptr_t>(clientIdentifier)));
commandLineVector.append('\0');
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
PROCESS_INFORMATION processInformation = { 0 };
BOOL result = ::CreateProcessW(0, commandLineVector.data(), 0, 0, true, 0, 0, 0, &startupInfo, &processInformation);
// We can now close the client identifier handle.
::CloseHandle(clientIdentifier);
if (!result) {
// FIXME: What should we do here?
DWORD error = ::GetLastError();
ASSERT_NOT_REACHED();
}
// Don't leak the thread handle.
::CloseHandle(processInformation.hThread);
// We've finished launching the process, message back to the run loop.
RunLoop::main()->scheduleWork(WorkItem::create(this, &ProcessLauncher::didFinishLaunchingProcess, processInformation.hProcess, serverIdentifier));
}
</pre>
<br />
<br />
<code>CoreIPC::Connection::createServerAndClientIdentifiers</code> creates a platform primitive for IPC (InterProcess Communication) between the UI Process and Web Processes. The whole implementation of IPC for WebKit2 is found in the <a href="http://trac.webkit.org/browser/trunk/WebKit2/Platform/CoreIPC" target="_blank">CoreIPC</a> directory. The <a href="http://trac.webkit.org/browser/trunk/WebKit2/Platform/CoreIPC/win/ConnectionWin.cpp" target="_blank">Windows version of <code>Connection::createServerAndClientIdentifiers</code></a> uses <a href="http://msdn.microsoft.com/en-us/library/aa365590.aspx" target="_blank">Named Pipe</a> for its IPC channel.<br />
<br />
<pre class="brush: cpp">bool Connection::createServerAndClientIdentifiers(HANDLE& serverIdentifier, HANDLE& clientIdentifier)
{
String pipeName;
while (true) {
unsigned uniqueID = randomNumber() * std::numeric_limits<unsigned>::max();
pipeName = String::format("\\\\.\\pipe\\com.apple.WebKit.%x", uniqueID);
serverIdentifier = ::CreateNamedPipe(pipeName.charactersWithNullTermination(),
PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE, 1, inlineMessageMaxSize, inlineMessageMaxSize,
0, 0);
if (!serverIdentifier && ::GetLastError() == ERROR_PIPE_BUSY) {
// There was already a pipe with this name, try again.
continue;
}
break;
}
if (!serverIdentifier)
return false;
clientIdentifier = ::CreateFileW(pipeName.charactersWithNullTermination(), GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);
if (!clientIdentifier) {
::CloseHandle(serverIdentifier);
return false;
}
DWORD mode = PIPE_READMODE_MESSAGE;
if (!::SetNamedPipeHandleState(clientIdentifier, &mode, 0, 0)) {
::CloseHandle(serverIdentifier);
::CloseHandle(clientIdentifier);
return false;
}
return true;
}
</pre>
<br />
<br />
In Chromium, a similar operation is handled by its IPC <a href="http://src.chromium.org/viewvc/chrome/trunk/src/ipc/ipc_channel_win.cc" target="_blank"><code>Channel</code></a> implementation. It may be interesting to compare it with the one in WebKit2.<br />
<br />
<pre class="brush: cpp">bool Channel::ChannelImpl::CreatePipe(const std::string& channel_id,
Mode mode) {
DCHECK(pipe_ == INVALID_HANDLE_VALUE);
const std::wstring pipe_name = PipeName(channel_id);
if (mode == MODE_SERVER) {
SECURITY_ATTRIBUTES security_attributes = {0};
security_attributes.bInheritHandle = FALSE;
security_attributes.nLength = sizeof(SECURITY_ATTRIBUTES);
if (!GetLogonSessionOnlyDACL(
reinterpret_cast<SECURITY_DESCRIPTOR**>(
&security_attributes.lpSecurityDescriptor))) {
NOTREACHED();
}
pipe_ = CreateNamedPipeW(pipe_name.c_str(),
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED |
FILE_FLAG_FIRST_PIPE_INSTANCE,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE,
1, // number of pipe instances
// output buffer size (XXX tune)
Channel::kReadBufferSize,
// input buffer size (XXX tune)
Channel::kReadBufferSize,
5000, // timeout in milliseconds (XXX tune)
&security_attributes);
LocalFree(security_attributes.lpSecurityDescriptor);
} else {
pipe_ = CreateFileW(pipe_name.c_str(),
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION |
FILE_FLAG_OVERLAPPED,
NULL);
}
if (pipe_ == INVALID_HANDLE_VALUE) {
// If this process is being closed, the pipe may be gone already.
LOG(WARNING) << "failed to create pipe: " << GetLastError();
return false;
}
// Create the Hello message to be sent when Connect is called
scoped_ptr<Message> m(new Message(MSG_ROUTING_NONE,
HELLO_MESSAGE_TYPE,
IPC::Message::PRIORITY_NORMAL));
if (!m->WriteInt(GetCurrentProcessId())) {
CloseHandle(pipe_);
pipe_ = INVALID_HANDLE_VALUE;
return false;
}
output_queue_.push(m.release());
return true;
}
</pre>
<br />
<br />
Going back to <code>ProcessLauncher::launchProcess</code>, it locates the executable module for the Web Process and creates a new process with the given parameters. When it's done, the callback function <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/Launcher/ProcessLauncher.cpp" target="_blank"><code>ProcessLauncher::didFinishLaunchingProcess</code></a> is put on the work queue of the <code>RunLoop</code>. It then calls <code>WebProcessProxy::didFinishLaunching</code> since m_client is actually <code>WebProcessProxy</code> as you may notice if you remember what was going on in <code>WebProcessProxy::connect</code>. <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebProcessProxy.h" target="_blank"><code>WebProcessProxy</code></a> inherits <code>ProcessLauncher::Client</code>.<br />
<br />
<pre class="brush: cpp">void ProcessLauncher::didFinishLaunchingProcess(PlatformProcessIdentifier processIdentifier, CoreIPC::Connection::Identifier identifier)
{
m_processIdentifier = processIdentifier;
m_isLaunching = false;
if (!m_client) {
// FIXME: Dispose of the connection identifier.
return;
}
m_client->didFinishLaunching(this, identifier);
}
</pre>
<br />
<br />
It then opens an IPC server connection (<code>RefPtr<CoreIPC::Connection> m_connection</code>) in <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebProcessProxy.cpp" target="_blank"><code>WebProcessProxy::didFinishLaunching</code></a>.<br />
<br />
<pre class="brush: cpp">void WebProcessProxy::didFinishLaunching(ProcessLauncher*, CoreIPC::Connection::Identifier connectionIdentifier)
{
didFinishLaunching(connectionIdentifier);
}
void WebProcessProxy::didFinishLaunching(ThreadLauncher*, CoreIPC::Connection::Identifier connectionIdentifier)
{
didFinishLaunching(connectionIdentifier);
}
void WebProcessProxy::didFinishLaunching(CoreIPC::Connection::Identifier connectionIdentifier)
{
ASSERT(!m_connection);
m_connection = CoreIPC::Connection::createServerConnection(connectionIdentifier, this, RunLoop::main());
m_connection->open();
for (size_t i = 0; i < m_pendingMessages.size(); ++i) {
CoreIPC::Connection::OutgoingMessage& outgoingMessage = m_pendingMessages[i];
m_connection->sendMessage(outgoingMessage.messageID(), adoptPtr(outgoingMessage.arguments()));
}
m_pendingMessages.clear();
// Tell the context that we finished launching.
m_context->processDidFinishLaunching(this);
}
</pre>
<br />
<br />
The IPC server in the UI Process hosts Web Processes. <code>Connection::open</code> is also implemented for a specific platform. Its <a href="http://trac.webkit.org/browser/trunk/WebKit2/Platform/CoreIPC/win/ConnectionWin.cpp" target="_blank">Windows version</a> starts listening for read and write state events on event handles to communicate with Web Processes. It's a very standard server behavior.<br />
<br />
<pre class="brush: cpp">bool Connection::open()
{
// Start listening for read and write state events.
m_connectionQueue.registerHandle(m_readState.hEvent, WorkItem::create(this, &Connection::readEventHandler));
m_connectionQueue.registerHandle(m_writeState.hEvent, WorkItem::create(this, &Connection::writeEventHandler));
// Schedule a read.
m_connectionQueue.scheduleWork(WorkItem::create(this, &Connection::readEventHandler));
return true;
}
</pre>
<br />
<br />
Finally, <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebContext.cpp" target="_blank"><code>WebContext::processDidFinishLaunching</code></a> is called to do some preparation for its internal history and pending IPC messages.<br />
<br />
<pre class="brush: cpp">void WebContext::processDidFinishLaunching(WebProcessProxy* process)
{
// FIXME: Once we support multiple processes per context, this assertion won't hold.
ASSERT(process == m_process);
m_visitedLinkProvider.populateVisitedLinksIfNeeded();
for (size_t i = 0; i != m_pendingMessagesToPostToInjectedBundle.size(); ++i) {
pair<String, RefPtr<APIObject> >& message = m_pendingMessagesToPostToInjectedBundle[i];
m_process->send(InjectedBundleMessage::PostMessage, 0, CoreIPC::In(message.first, WebContextUserMessageEncoder(message.second.get())));
}
m_pendingMessagesToPostToInjectedBundle.clear();
}
</pre>
<br />
<br />
The above call stack is what happens when a new <code>WebContext</code> is initialized in <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser/win/BrowserView.cpp" target="_blank"><code>BrowserView::create</code></a>. The next thing it does is to call the <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/API/C/WKPageNamespace.cpp" target="_blank"><code>WKPageNamespaceCreate</code></a> API.<br />
<br />
<pre class="brush: cpp">WKPageNamespaceRef WKPageNamespaceCreate(WKContextRef ownerContextRef)
{
return toAPI(toImpl(ownerContextRef)->createPageNamespace());
}
</pre>
<br />
<br />
It calls <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebContext.cpp" target="_blank"><code>WebContext::createPageNamespace</code></a> then <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebPageNamespace.h" target="_blank"><code>WebPageNamespace::create</code></a>.<br />
<br />
<pre class="brush: cpp">class WebPageNamespace : public APIObject {
public:
static const Type APIType = TypePageNamespace;
static PassRefPtr<WebPageNamespace> create(WebContext* context)
{
return adoptRef(new WebPageNamespace(context));
}
</pre>
<br />
<br />
For now it seems <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebPageNamespace.cpp" target="_blank"><code>WebPageNamespace</code></a> does nothing when created except for some reference counting.<br />
<br />
<pre class="brush: cpp">#ifndef NDEBUG
static WTF::RefCountedLeakCounter webPageNamespaceCounter("WebPageNamespace");
#endif
WebPageNamespace::WebPageNamespace(WebContext* context)
: m_context(context)
{
#ifndef NDEBUG
webPageNamespaceCounter.increment();
#endif
}
</pre>
<br />
<br />
After creating a namespace for scripting, <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser/win/BrowserView.cpp" target="_blank"><code>BrowserView::create</code></a> calls the <code>WKViewCreate</code> API. For Windows, it's just a <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/API/C/win/WKView.cpp" target="_blank">call</a> to <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/win/WebView.h" target="_blank"><code>WebView::create</code></a> which instanciates <code>WebView</code> via its <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/win/WebView.cpp" target="_blank">constructor</a>.<br />
<br />
<pre class="brush: cpp">WKViewRef WKViewCreate(RECT rect, WKPageNamespaceRef pageNamespaceRef, HWND parentWindow)
{
RefPtr<WebView> view = WebView::create(rect, toImpl(pageNamespaceRef), parentWindow);
return toAPI(view.release().releaseRef());
}
</pre>
<br />
<br />
<pre class="brush: cpp">class WebView : public APIObject, public PageClient, WebCore::WindowMessageListener {
public:
static PassRefPtr<WebView> create(RECT rect, WebPageNamespace* pageNamespace, HWND parentWindow)
{
return adoptRef(new WebView(rect, pageNamespace, parentWindow));
}
~WebView();
// snip
virtual HWND nativeWindow();
// WebCore::WindowMessageListener
virtual void windowReceivedMessage(HWND, UINT message, WPARAM, LPARAM);
RECT m_rect;
HWND m_window;
HWND m_topLevelParentWindow;
HWND m_toolTipWindow;
HCURSOR m_lastCursorSet;
HCURSOR m_webCoreCursor;
HCURSOR m_overrideCursor;
bool m_trackingMouseLeave;
bool m_isBeingDestroyed;
RefPtr<WebPageProxy> m_page;
};
</pre>
<br />
<br />
<pre class="brush: cpp">WebView::WebView(RECT rect, WebPageNamespace* pageNamespace, HWND parentWindow)
: m_rect(rect)
, m_topLevelParentWindow(0)
, m_toolTipWindow(0)
, m_lastCursorSet(0)
, m_webCoreCursor(0)
, m_overrideCursor(0)
, m_trackingMouseLeave(false)
, m_isBeingDestroyed(false)
{
registerWebViewWindowClass();
m_page = pageNamespace->createWebPage();
m_page->setPageClient(this);
m_page->setDrawingArea(ChunkedUpdateDrawingAreaProxy::create(this));
m_window = ::CreateWindowEx(0, kWebKit2WebViewWindowClassName, 0, WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
rect.top, rect.left, rect.right - rect.left, rect.bottom - rect.top, parentWindow ? parentWindow : HWND_MESSAGE, 0, instanceHandle(), this);
ASSERT(::IsWindow(m_window));
m_page->initializeWebPage(IntRect(rect).size());
::ShowWindow(m_window, SW_SHOW);
// FIXME: Initializing the tooltip window here matches WebKit win, but seems like something
// we could do on demand to save resources.
initializeToolTipWindow();
// Initialize the top level parent window and register it with the WindowMessageBroadcaster.
windowAncestryDidChange();
}
</pre>
<br />
<br />
<code>WebView</code> has a native window handle (HWND) and a reference to a <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebPageProxy.h" target="_blank"><code>WebPageProxy</code></a> object which is also initialized here. It has a <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/DrawingAreaProxy.h" target="_blank"><code>DrawingAreaProxy</code></a> as a member. Sometimes it receives an inter-process draw message via <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/WebPageProxy.cpp" target="_blank"><code>WebPageProxy::didReceiveMessage</code></a>. <code>DrawingAreaProxy</code> is the proxy in the UI Process to the <a href="http://trac.webkit.org/browser/trunk/WebKit2/WebProcess/WebPage/DrawingArea.h" target="_blank"><code>DrawingArea</code></a> object in a Web Process.<br />
<br />
<pre class="brush: cpp">void WebPageProxy::didReceiveMessage(CoreIPC::Connection* connection, CoreIPC::MessageID messageID, CoreIPC::ArgumentDecoder* arguments)
{
if (messageID.is<CoreIPC::MessageClassDrawingAreaProxy>()) {
m_drawingArea->didReceiveMessage(connection, messageID, arguments);
return;
}
didReceiveWebPageProxyMessage(connection, messageID, arguments);
}
</pre>
<br />
<br />
From here on, I'll show you what the graphics update loop is like in WebKit2. You'd notice that in <code>WebView::WebView</code> the <code>WebPageProxy</code> object is initialized with <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/ChunkedUpdateDrawingAreaProxy.h" target="_blank"><code>ChunkedUpdateDrawingAreaProxy</code></a> as its <code>DrawingAreaProxy</code> object. It is the default drawing strategy for the Windows version. It is a proxy to a <a href="http://trac.webkit.org/browser/trunk/WebKit2/WebProcess/WebPage/ChunkedUpdateDrawingArea.cpp" target="_blank"><code>ChunkedUpdateDrawingArea</code></a> object in a Web Process.<br />
<br />
<pre class="brush: cpp">ChunkedUpdateDrawingArea::ChunkedUpdateDrawingArea(DrawingAreaID identifier, WebPage* webPage)
: DrawingArea(ChunkedUpdateDrawingAreaType, identifier, webPage)
, m_isWaitingForUpdate(false)
, m_paintingIsSuspended(false)
, m_displayTimer(WebProcess::shared().runLoop(), this, &ChunkedUpdateDrawingArea::display)
{
}
// snip
void ChunkedUpdateDrawingArea::scheduleDisplay()
{
if (m_paintingIsSuspended)
return;
if (m_isWaitingForUpdate)
return;
if (m_dirtyRect.isEmpty())
return;
if (m_displayTimer.isActive())
return;
m_displayTimer.startOneShot(0);
}
</pre>
<br />
<br />
It schedules the first update of the drawing area by pushing the <code>ChunkedUpdateDrawingArea::display</code> function into the <code>RunLoop</code>.<br />
<br />
<pre class="brush: cpp">void ChunkedUpdateDrawingArea::display()
{
ASSERT(!m_isWaitingForUpdate);
if (m_paintingIsSuspended)
return;
if (m_dirtyRect.isEmpty())
return;
// Layout if necessary.
m_webPage->layoutIfNeeded();
IntRect dirtyRect = m_dirtyRect;
m_dirtyRect = IntRect();
// Create a new UpdateChunk and paint into it.
UpdateChunk updateChunk(dirtyRect);
paintIntoUpdateChunk(&updateChunk);
WebProcess::shared().connection()->send(DrawingAreaProxyMessage::Update, m_webPage->pageID(), CoreIPC::In(updateChunk));
m_isWaitingForUpdate = true;
m_displayTimer.stop();
}
</pre>
<br />
<br />
When there is a dirty rect as the result of modified page layout, it creates an <code>UpdateChunk</code> object. The Windows version of <a href="http://trac.webkit.org/browser/trunk/WebKit2/Shared/win/UpdateChunk.cpp" target="_blank"><code>UpdateChunk</code></a> is a shared memory bitmap created with the <a href="http://msdn.microsoft.com/en-us/library/aa366537.aspx" target="_blank"><code>CreateFileMapping</code></a> API to make it accessible from multiple processes. Then the content of the dirty rect is written into the bitmap with <a href="http://trac.webkit.org/browser/trunk/WebKit2/WebProcess/WebPage/win/ChunkedUpdateDrawingAreaWin.cpp" target="_blank"><code>ChunkedUpdateDrawingArea::paintIntoUpdateChunk</code></a>.<br />
<br />
<pre class="brush: cpp">UpdateChunk::UpdateChunk(const IntRect& rect)
: m_rect(rect)
{
// Create our shared memory mapping.
unsigned memorySize = rect.height() * rect.width() * 4;
m_bitmapSharedMemory = ::CreateFileMapping(INVALID_HANDLE_VALUE, 0, PAGE_READWRITE, 0, memorySize, 0);
}
</pre>
<br />
<br />
<pre class="brush: cpp">void ChunkedUpdateDrawingArea::paintIntoUpdateChunk(UpdateChunk* updateChunk)
{
OwnPtr<HDC> hdc(::CreateCompatibleDC(0));
void* bits;
BitmapInfo bmp = BitmapInfo::createBottomUp(updateChunk->rect().size());
OwnPtr<HBITMAP> hbmp(::CreateDIBSection(0, &bmp, DIB_RGB_COLORS, &bits, updateChunk->memory(), 0));
HBITMAP hbmpOld = static_cast<HBITMAP>(::SelectObject(hdc.get(), hbmp.get()));
GraphicsContext gc(hdc.get());
gc.save();
// FIXME: Is this white fill needed?
RECT rect = updateChunk->rect();
::FillRect(hdc.get(), &rect, (HBRUSH)::GetStockObject(WHITE_BRUSH));
gc.translate(-updateChunk->rect().x(), -updateChunk->rect().y());
m_webPage->drawRect(gc, updateChunk->rect());
gc.restore();
// Re-select the old HBITMAP
::SelectObject(hdc.get(), hbmpOld);
}
</pre>
<br />
<br />
When it's done, the <code>DrawingAreaProxyMessage::Update</code> IPC message is sent to the <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/ChunkedUpdateDrawingAreaProxy.h" target="_blank"><code>ChunkedUpdateDrawingAreaProxy</code></a> in the UI Process with the bitmap created. It receives the message in <code>ChunkedUpdateDrawingAreaProxy::didReceiveMessage</code> and decodes the bitmap packed as an IPC payload.<br />
<br />
<pre class="brush: cpp">void ChunkedUpdateDrawingAreaProxy::update(UpdateChunk* updateChunk)
{
if (!m_isVisible) {
// We got an update request that must have been sent before we told the web process to suspend painting.
// Don't paint this into the backing store, because that could leave the backing store in an inconsistent state.
// Instead, we will just tell the drawing area to repaint everything when we resume painting.
m_forceRepaintWhenResumingPainting = true;
} else {
// Just paint into backing store.
drawUpdateChunkIntoBackingStore(updateChunk);
}
WebPageProxy* page = this->page();
page->process()->send(DrawingAreaMessage::DidUpdate, page->pageID(), CoreIPC::In(info().id));
}
void ChunkedUpdateDrawingAreaProxy::didReceiveMessage(CoreIPC::Connection*, CoreIPC::MessageID messageID, CoreIPC::ArgumentDecoder* arguments)
{
switch (messageID.get<DrawingAreaProxyMessage::Kind>()) {
case DrawingAreaProxyMessage::Update: {
UpdateChunk updateChunk;
if (!arguments->decode(updateChunk))
return;
update(&updateChunk);
break;
}
case DrawingAreaProxyMessage::DidSetSize: {
UpdateChunk updateChunk;
if (!arguments->decode(CoreIPC::Out(updateChunk)))
return;
didSetSize(&updateChunk);
break;
}
default:
ASSERT_NOT_REACHED();
}
}
</pre>
<br />
<br />
Then, in <code>ChunkedUpdateDrawingAreaProxy::update</code> it calls <code>ChunkedUpdateDrawingAreaProxy::drawUpdateChunkIntoBackingStore</code> to BitBlt from the updated image to the backing store.<br />
<br />
<pre class="brush: cpp">void ChunkedUpdateDrawingAreaProxy::drawUpdateChunkIntoBackingStore(UpdateChunk* updateChunk)
{
ensureBackingStore();
OwnPtr<HDC> updateChunkBitmapDC(::CreateCompatibleDC(m_backingStoreDC.get()));
// Create a bitmap.
BitmapInfo bitmapInfo = BitmapInfo::createBottomUp(updateChunk->rect().size());
// Duplicate the update chunk handle.
HANDLE updateChunkHandle;
BOOL result = ::DuplicateHandle(m_webView->page()->process()->processIdentifier(), updateChunk->memory(),
::GetCurrentProcess(), &updateChunkHandle, STANDARD_RIGHTS_REQUIRED | FILE_MAP_READ | FILE_MAP_WRITE, false, DUPLICATE_CLOSE_SOURCE);
void* pixels = 0;
OwnPtr<HBITMAP> hBitmap(::CreateDIBSection(0, &bitmapInfo, DIB_RGB_COLORS, &pixels, updateChunkHandle, 0));
::SelectObject(updateChunkBitmapDC.get(), hBitmap.get());
// BitBlt from the UpdateChunk to the backing store.
::BitBlt(m_backingStoreDC.get(), updateChunk->rect().x(), updateChunk->rect().y(), updateChunk->rect().width(), updateChunk->rect().height(), updateChunkBitmapDC.get(), 0, 0, SRCCOPY);
// FIXME: We should not do this here.
::CloseHandle(updateChunkHandle);
// Invalidate the WebView's HWND.
RECT rect = updateChunk->rect();
::InvalidateRect(m_webView->window(), &rect, false);
}
</pre>
<br />
<br />
As the event notification, the <code>DrawingAreaMessage::DidUpdate</code> IPC message is sent to the Web Process which triggers a subsequent drawing operation.<br />
<br />
Now the page view is ready. After that, in <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser/win/BrowserView.cpp" target="_blank"><code>BrowserView::create</code></a>, it sets application-defined callbacks with the <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/API/C/WKPage.h" target="_blank"><code>WKPageSetPageUIClient</code></a> API to invoke appropriate UI elements on events returned from WebKit.<br />
<br />
<pre class="brush: cpp">WKPageUIClient uiClient = {
0, /* version */
parentWindow, /* clientInfo */
createNewPage,
showPage,
closePage,
runJavaScriptAlert,
runJavaScriptConfirm,
runJavaScriptPrompt,
setStatusText,
mouseDidMoveOverElement,
0, /* didNotHandleKeyEvent */
0, /* toolbarsAreVisible */
0, /* setToolbarsAreVisible */
0, /* menuBarIsVisible */
0, /* setMenuBarIsVisible */
0, /* statusBarIsVisible */
0, /* setStatusBarIsVisible */
0, /* isResizable */
0, /* setIsResizable */
0, /* getWindowFrame */
0, /* setWindowFrame */
0, /* runBeforeUnloadConfirmPanel */
0, /* didDraw */
0 /* pageDidScroll */
};
WKPageSetPageUIClient(WKViewGetPage(m_webView), &uiClient);
</pre>
<br />
<br />
Though this article uses the Windows version as the primary example, the <a href="http://trac.webkit.org/browser/trunk/WebKitTools/MiniBrowser/mac/BrowserWindowController.m" target="_blank">Mac version of MiniBrowser</a> in Objective-C has more features implemented already by using other WebKit2 APIs. Besides <code>WKPageUIClient</code>, <a href="http://trac.webkit.org/browser/trunk/WebKit2/UIProcess/API/C/WKPage.h" target="_blank"><code>WKPage.h</code></a> offers other kinds of clients such as <code>WKPageLoaderClient</code>, <code>WKPagePolicyClient</code> and <code>WKPageFindClient</code> for embedders to customize the application behavior by defining and plugging in their own callback functions.<br />
<br />
As for other multiprocess-related implementation details, the Web Process has a <a href="http://trac.webkit.org/browser/trunk/WebKit2/Shared/VisitedLinkTable.cpp" target="_blank"><code>VisitedLinkTable</code></a> object as its member. Its hash table is on the <a href="http://trac.webkit.org/browser/trunk/WebKit2/Platform/win/SharedMemoryWin.cpp?" target="_blank"><code>SharedMemory</code></a> object for cross-process update.<br />
<br />
Examples of WebKit2 usage are also found in the <a href="http://trac.webkit.org/browser/trunk/WebKitTools/TestWebKitAPI/Tests/WebKit2" target="_blank">tests for the WebKit2 API</a> so browse them if you are interested.<br />
<br />
Since WebKit2 is in its very early stage, the code size is still kept compact so reading its source code and comprehending its structure is super easy and fun. Going back to my original question at the beginning of this article, it seems the basic functions of WebKit2 are all in after 6 months. Though my personal attention has been on Chromium development rather than on WebKit, it may be interesting to see how these 2 interact each other in future.
久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-81626185974565825572010-09-11T08:02:00.000+09:002017-05-01T05:39:31.421+09:00Preliminary Look into libevent 2 for WindowsThe <a href="http://monkey.org/~provos/libevent/" target="_blank">libevent</a> library is probably best known by its use in <a href="http://memcached.org/" target="_blank">memcached</a>. memcached is one of the key components of the LAMP stack. However the recent trend seems to instead highlight NoSQL solutions where typical RDBMS and memcached are replaced with more optimized and dedicated systems. The importance of libevent is not affected at all by that trend since it's not tied to any applications and can be used for NoSQL solutions. <a href="http://www.torproject.org/" target="_blank">Tor</a> is another use case of libevent. libevent is crucial for its cross-platform availability. One of the developers of libevent is Nick Mathewson who is also the key person behind the Tor project.<br />
<br />
My interest in libevent stems from my own project, the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a>. From the beginning of the development in 2002, its I/O is fully optimized to Windows and <a href="http://msdn.microsoft.com/en-us/library/aa365198.aspx" target="_blank">I/O Completion Ports</a> (IOCP) without using any middleware or libraries. Even though it works flawlessly in its current state, replacing it with a decent OSS library can be a good chance to make it more robust and future-proof. The only concern was about its performance, but libevent 2 is supposed to come with the IOCP support. The upcoming IOCP support led me to start the evaluation of the library.<br />
<br />
The libevent version for this article is 2.0.7 RC. I built it with Visual Studio 2010 (VC10). Since I wanted to use the debugger to trace its flow, I built it as the debug version by changing the CFLAGS compiler options <code>/Ox</code> to <code>/Od /MDd /Zi</code> in the Makefile.nmake in the main directory and the test directory. '<code>nmake -f Makefile.nmake</code>' produces the libevent.lib static library.<br />
<br />
Before building a project with libevent, you need to manually copy WIN32-Code\event2\event-config.h to include\event2\event-config.h. Then add libevent.lib to the linker input (if you use socket, ws2_32.lib too), and add libevent and libevent\include directories to the include directory.<br />
<br />
Then I wrote a test code with its evhttp 'Event-driven HTTP servers' functions. It's a simple Web server that shows requested URI and quits if /quit is requested. It listens to the port 8080.<br />
<br />
<a name='more'></a><br />
<br />
<pre class="brush: cpp">#include <iostream>
#include <cmath>
#include <ctime>
#include <event.h>
#include <evhttp.h>
#include <event2/thread.h>
void handlerRoot(evhttp_request* request, void* args)
{
std::cout << "Request URI: " << request->uri << std::endl;
evbuffer* buffer = evbuffer_new();
evkeyvalq* headers = evhttp_request_get_output_headers(request);
evhttp_add_header(headers, "Content-Type", "text/html; charset=UTF-8");
evbuffer_add_printf(buffer, "Request URI: %s", request->uri);
evhttp_send_reply(request, HTTP_OK, "OK", buffer);
evbuffer_free(buffer);
}
void handlerQuit(evhttp_request* request, void* args)
{
event_base* eventBase = (event_base*)args;
event_base_loopbreak(eventBase);
}
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsaData = {0};
::WSAStartup(MAKEWORD(2, 2), &wsaData);
event_config* eventConfig = event_config_new();
event_config_set_flag(eventConfig, EVENT_BASE_FLAG_STARTUP_IOCP);
event_config_set_num_cpus_hint(eventConfig, 8);
event_base* eventBase = event_base_new_with_config(eventConfig);
evthread_use_windows_threads();
evhttp* eventHttp = evhttp_new(eventBase);
evhttp_set_gencb(eventHttp, handlerRoot, eventBase);
evhttp_set_cb(eventHttp, "/quit", handlerQuit, eventBase);
evhttp_bind_socket(eventHttp, "0.0.0.0", 8080);
timeval tv = {::pow(2.0, 31) - ::time(NULL) - 1000, 0};
event_base_loopexit(eventBase, &tv);
event_base_dispatch(eventBase);
evhttp_free(eventHttp);
event_base_free(eventBase);
::WSACleanup();
return 0;
}</pre>
<br />
<br />
The document is not yet complete for the recently added functions and I had to read the libevent code to add some function calls that enable IOCP. As the whatsnew-2.0.txt suggests, it's disabled by default. The wierdest part is probably this:<br />
<br />
<pre class="brush: cpp"> timeval tv = {::pow(2.0, 31) - ::time(NULL) - 1000, 0};
event_base_loopexit(eventBase, &tv);
event_base_dispatch(eventBase);</pre>
<br />
<br />
<code>event_base_dispatch</code> starts the event dispatching loop, but if you enable IOCP it doesn't block and the program just exits. So you have to manually set its lifetime. I tried to set the largest signed integer (::pow(2.0, 31)), but unfortunately <code>event_base_loopexit</code> internally calls <code>event_base_once</code> which calls <code>event_add_internal</code> with tv_is_absolute = 0. The tricky code above is just a quick fix.<br />
<br />
<pre class="brush: cpp">/* Implementation function to add an event. Works just like event_add,
* except: 1) it requires that we have the lock. 2) if tv_is_absolute is set,
* we treat tv as an absolute time, not as an interval to add to the current
* time */
static inline int
event_add_internal(struct event *ev, const struct timeval *tv,
int tv_is_absolute)
{</pre>
<br />
<br />
<code>event_config_set_num_cpus_hint</code> sets the CPU number as 8, it's because I use Core i7 with 8 SMT threads. Internally, it spawns (num_cpus * 2) threads in its thread pool for IOCP.<br />
<br />
<pre class="brush: cpp">struct event_iocp_port *
event_iocp_port_launch(int n_cpus)
{
struct event_iocp_port *port;
int i;
if (!extension_fns_initialized)
init_extension_functions(&the_extension_fns);
if (!(port = mm_calloc(1, sizeof(struct event_iocp_port))))
return NULL;
if (n_cpus <= 0)
n_cpus = N_CPUS_DEFAULT;
port->n_threads = n_cpus * 2;
port->threads = calloc(port->n_threads, sizeof(HANDLE));
if (!port->threads)
goto err;
port->port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0,
n_cpus);
port->ms = -1;
if (!port->port)
goto err;
port->shutdownSemaphore = CreateSemaphore(NULL, 0, 1, NULL);
if (!port->shutdownSemaphore)
goto err;
for (i=0; i<port->n_threads; ++i) {
ev_uintptr_t th = _beginthread(loop, 0, port);
if (th == (ev_uintptr_t)-1)
goto err;
port->threads[i] = (HANDLE)th;
++port->n_live_threads;
}
InitializeCriticalSectionAndSpinCount(&port->lock, 1000);
return port;
err:
if (port->port)
CloseHandle(port->port);
if (port->threads)
mm_free(port->threads);
if (port->shutdownSemaphore)
CloseHandle(port->shutdownSemaphore);
mm_free(port);
return NULL;
}</pre>
<br />
<br />
The threads in the pool then block with <a href="http://msdn.microsoft.com/en-us/library/aa364986.aspx" target="_blank"><code>GetQueuedCompletionStatus</code></a> that waits for I/O completion notification.<br />
<br />
<pre class="brush: cpp">static void
loop(void *_port)
{
struct event_iocp_port *port = _port;
long ms = port->ms;
HANDLE p = port->port;
if (ms <= 0)
ms = INFINITE;
while (1) {
OVERLAPPED *overlapped=NULL;
ULONG_PTR key=0;
DWORD bytes=0;
int ok = GetQueuedCompletionStatus(p, &bytes, &key,
&overlapped, ms);
EnterCriticalSection(&port->lock);
if (port->shutdown) {
if (--port->n_live_threads == 0)
ReleaseSemaphore(port->shutdownSemaphore, 1,
NULL);
LeaveCriticalSection(&port->lock);
return;
}
LeaveCriticalSection(&port->lock);
if (key != NOTIFICATION_KEY && overlapped)
handle_entry(overlapped, key, bytes, ok);
else if (!overlapped)
break;
}
event_warnx("GetQueuedCompletionStatus exited with no event.");
EnterCriticalSection(&port->lock);
if (--port->n_live_threads == 0)
ReleaseSemaphore(port->shutdownSemaphore, 1, NULL);
LeaveCriticalSection(&port->lock);
}</pre>
<br />
<br />
So I built the above code and ran it. It worked. But when I set breakpoints in the debugger, it turned out that things were not working as expected. It breaks at the line after <code>GetQueuedCompletionStatus</code> only when accepting a new connection. I suspected only the accept operation was overlapped.<br />
<br />
<pre class="brush: cpp">static int
start_accepting(struct accepting_socket *as)
{
/* requires lock */
const struct win32_extension_fns *ext = event_get_win32_extension_fns();
DWORD pending = 0;
SOCKET s = socket(as->family, SOCK_STREAM, 0);
if (s == INVALID_SOCKET)
return -1;
setsockopt(s, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT,
(char *)&as->lev->fd, sizeof(&as->lev->fd));
if (!(as->lev->base.flags & LEV_OPT_LEAVE_SOCKETS_BLOCKING))
evutil_make_socket_nonblocking(s);
if (event_iocp_port_associate(as->lev->port, s, 1) < 0) {
closesocket(s);
return -1;
}
as->s = s;
if (ext->AcceptEx(as->lev->fd, s, as->addrbuf, 0,
as->buflen/2, as->buflen/2, &pending, &as->overlapped.overlapped))
{
/* Immediate success! */
accepted_socket_cb(&as->overlapped, 1, 0, 1);
} else {
int err = WSAGetLastError();
if (err != ERROR_IO_PENDING) {
event_warnx("AcceptEx: %s", evutil_socket_error_to_string(err));
return -1;
}
}
return 0;
}</pre>
<br />
<br />
In the debugger, it surely calls <a href="http://msdn.microsoft.com/en-us/library/ms737524.aspx" target="_blank"><code>AcceptEx</code></a> in <code>start_accepting</code>. The problem is, it uses the non-IOCP version of the read operation function.<br />
<br />
<pre class="brush: cpp">evbuffer_read(struct evbuffer *buf, evutil_socket_t fd, int howmuch)
{
// snip
#ifdef WIN32
{
DWORD bytesRead;
DWORD flags=0;
if (WSARecv(fd, vecs, nvecs, &bytesRead, &flags, NULL, NULL)) {
/* The read failed. It might be a close,
* or it might be an error. */
if (WSAGetLastError() == WSAECONNABORTED)
n = 0;
else
n = -1;
} else
n = bytesRead;
}
#else
n = readv(fd, vecs, nvecs);
#endif</pre>
<br />
<br />
<pre class="brush: cpp">int
evbuffer_launch_read(struct evbuffer *buf, size_t at_most,
struct event_overlapped *ol)
{
// snip
_evbuffer_incref(buf);
if (WSARecv(buf_o->fd, buf_o->buffers, nvecs, &bytesRead, &flags,
&ol->overlapped, NULL)) {
int error = WSAGetLastError();
if (error != WSA_IO_PENDING) {
/* An actual error. */
pin_release(buf_o, EVBUFFER_MEM_PINNED_R);
evbuffer_unfreeze(buf, 0);
evbuffer_free(buf); /* decref */
goto done;
}
}</pre>
<br />
<br />
The latter one gives an <a href="http://msdn.microsoft.com/en-us/library/ms741665.aspx" target="_blank"><code>OVERLAPPED</code></a> structure to initiate overlapped read, but it was not actually called.<br />
<br />
I tracked where it went wrong. The culprit was in <code>http.c</code>. It used the <code>bufferevent_new</code> that doesn't support IOCP.<br />
<br />
<pre class="brush: cpp">struct evhttp_connection *
evhttp_connection_base_new(struct event_base *base, struct evdns_base *dnsbase,
const char *address, unsigned short port)
{
// snip
if ((evcon->bufev = bufferevent_new(-1,
evhttp_read_cb,
evhttp_write_cb,
evhttp_error_cb, evcon)) == NULL) {
event_warn("%s: bufferevent_new failed", __func__);
goto error;
}</pre>
<br />
<br />
Instead, it has to use <code>bufferevent_socket_new</code>. But other parts of <code>http.c</code> have to be modified to work correctly, so I gave up there.<br />
<br />
<pre class="brush: cpp">struct bufferevent *
bufferevent_socket_new(struct event_base *base, evutil_socket_t fd,
int options)
{
struct bufferevent_private *bufev_p;
struct bufferevent *bufev;
#ifdef WIN32
if (base && event_base_get_iocp(base))
return bufferevent_async_new(base, fd, options);
#endif</pre>
<br />
<br />
Though my time ran out, you may test if the async read/write does work or not by running the iocp/bufferevent_async and other regression tests defined in <code>regress_iocp.c</code> in the test folder. Don't forget to add the <code>--no-fork</code> option.<br />
<br />
Will it be improved? I hope so, these IOCP details should be hidden from a user.<br />
<br />
Another thing to look at is the thread pool. Since Windows 2000, the <a href="http://msdn.microsoft.com/en-us/magazine/cc163327.aspx" target="_blank">thread pool</a> is an OS component that is optimized to the OS inner working. <a href="http://msdn.microsoft.com/en-us/library/aa363484.aspx" target="_blank"><code>BindIoCompletionCallback</code></a> is available since Windows 2000, and <a href="http://msdn.microsoft.com/en-us/library/ms682456.aspx" target="_blank"><code>CreateThreadpool</code></a> and other related API have been added in Windows Vista. It seems it's still far away that libevent is fully optimized to Windows.<br />
<br />
However, as an easy-going framework to save time, it's in a good state already as shown in my example. A bit more polishing will make it shine.久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-22240751280096461702010-08-26T13:48:00.000+09:002015-02-26T01:42:19.774+09:00js-ctypes実装に見るlibffiの利用(& Windows-MSVCでのビルド法)Firefox 3.6から入った、chrome権限のJavaScriptから外部バイナリコンポーネントのネイティブ関数を呼び出す機構である<a href="https://wiki.mozilla.org/JSctypes" target="_blank">js-ctypes</a>の実装が、<a href="http://sourceware.org/libffi/" target="_blank">libffi</a>に基づいているということだったので、昔見たときはlibffiはVisual C++はサポートしてなかった記憶があったがFirefoxでビルドできるんだったら単体でもいけるよねーということで<a href="http://mxr.mozilla.org/mozilla-central/source/js/src/ctypes/" target="_blank">js-ctypesのソース</a>を追いつつ試してみた。<br />
<br />
結果から先に述べると、若干手間を要するものの問題なくビルドでき、closureも利用できた。ここでいうclosureとは、呼び出された際にユーザー指定コールバック関数(元の呼び出しに使われたlibffiフォーマットの引数データが渡ってくる)を起動してくれる関数コードを、ユーザーが動的に指定したシグニチャで、ランタイムに実行可能メモリ上に構築してくれるlibffiのAPIが用意されているので、それを利用して作成したバイナリコードを指す。<br />
<br />
js-ctypes実装の構造は至極直截的で、JSのオブジェクトとネイティブオブジェクトのバインダとしての<a href="http://mxr.mozilla.org/mozilla-central/source/js/src/ctypes/CTypes.h" target="_blank"><code>CTypes</code>クラス</a>と、dll/so/dylibみたいなバイナリコンポーネントのラッパーの<a href="http://mxr.mozilla.org/mozilla-central/source/js/src/ctypes/Library.h" target="_blank"><code>Library</code>クラス</a>から成っている。<code>Library::Declare</code>でシンボルからバイナリ内の関数アドレスを得るときは、<code>PR_FindFunctionSymbol</code>を呼ぶ。これは<a href="http://www.mozilla.org/projects/nspr/" target="_blank">NSPR</a>の関数で、Windows上では、なんのことはない<a href="http://mxr.mozilla.org/mozilla-central/source/nsprpub/pr/src/linking/prlink.c" target="_blank"><code>GetProcAddress</code>が呼ばれる</a>。なんでlibffiなんてものが必要かというと、荒っぽく言ってしまえば、Cでは関数ポインタを宣言しておけばコンパイラが良きに計らってくれるところ、動的言語で同じことをランタイムに行うために、その辺の呼び出し規約上のお膳立てをlibffiが整えてくれるというわけだ。<br />
<br />
上述のclosureは、js-ctypesでは、ネイティブコード側にJavascriptの関数をコールバックとして渡す時に利用されている。Javascript関数オブジェクトに関連づけたclosureを作成し、それが呼び出された場合には<a href="http://mxr.mozilla.org/mozilla-central/source/js/src/ctypes/CTypes.cpp" target="_blank"><code>CClosure::ClosureStub</code></a>が呼び出され、関連づけられていたJavascript関数オブジェクトが実行されるようになっている。closureは、コールバック用のプロキシとして働いており、この利用法自体は、<a href="http://github.com/atgreen/libffi/blob/master/src/x86/ffi.c" target="_blank">libffiの実装</a>内でffi_prep_closure_locの実体がFFI_INIT_TRAMPOLINE呼び出しで、closure用のメモリ領域が「トランポリン」と呼ばれていることからわかるように、想定の範囲内と言える。<br />
<br />
Windowsアプリのメッセージハンドラ=ウィンドウプロシージャとしておなじみの<a href="http://msdn.microsoft.com/en-us/library/ms633573.aspx" target="_blank"><code>WindowProc</code></a>コールバックは、js-ctypesでは以下のように表現される。<br />
<br />
<pre class="brush: js">
var WNDPROC = ctypes.FunctionType(ctypes.stdcall_abi, ctypes.int, [ctypes.voidptr_t, ctypes.int32_t, ctypes.int32_t, ctypes.int32_t]).ptr;</pre>
<br />
<br />
Windows APIの呼び出し規約は<code>WINAPI(__stdcall)</code>なので<code>stdcall_abi</code>が指定されている。これを元にしてlibffiがclosureを作成し、closureが起動されるとjs-ctypesが再度Javascriptの関数に戻して実行してやる。これが何を意味するかというと、js-ctypesがあればJavascriptでWindowsアプリのイベント駆動モデルすらも代替できる、つまりMFCとかイラネーよなということになる。これだけ強力なら、<a href="https://developer.mozilla.org/en/XPCOM/XPCOM_changes_in_Gecko_2.0" target="_blank">Firefox 4のGecko 2がfrozenインターフェイスを破棄</a>してjs-ctypesを媒介にXPCOMを脱構築するという流れも必然であると納得できる。<br />
<br />
Windows上でのVisual Studio 2010(VC10)を使ったlibffiのビルド手順としては、以下になる:<br />
<a name='more'></a><ol><br />
<li>Cygwinをインストール。デフォルトのパッケージに加え、Develのmakeが必要なので含めておく。(msysはcygpathが無いので駄目)</li>
<br />
<li>Gitをインストール。改行コードの扱いは、チェックアウト=as-is、チェックイン=LFで。cygwinのshは、configureやconfig.sub等のスクリプトが改行コードLFでないと処理できないため。</li>
<br />
<li>Gitで<a href="http://github.com/atgreen/libffi" target="_blank">libffiのGitHubレポジトリ</a>からソース取得。</li>
<br />
<li>Visual StudioのVisual Studio Toolsからコマンドプロンプトを起動。Cygwinのフォルダに移動し、Cygwin.batを起動。bash内で/cygdrive/の中のlibffiのソースが置いてあるディレクトリへ移動。</li>
<br />
<li>configureオプションについては、libffiのREADME内に簡単な指示はあるものの、Firefoxでのjs-ctypes用<a href="http://mxr.mozilla.org/mozilla-central/source/js/src/configure.in">configure.in</a>を参考にすべきだろう。<br /><pre class="brush: bash">
# Build jsctypes if it's enabled.
if test "$JS_HAS_CTYPES"; then
# Run the libffi 'configure' script.
ac_configure_args="--disable-shared --enable-static --disable-raw-api"
if test "$MOZ_DEBUG"; then
ac_configure_args="$ac_configure_args --enable-debug"
fi
if test "$DSO_PIC_CFLAGS"; then
ac_configure_args="$ac_configure_args --with-pic"
fi
if test "$CROSS_COMPILE"; then
case "$target" in
*-mingw*)
ac_configure_args="$ac_configure_args --build=$build --host=${target_cpu}-${target_os} HOST_CC=\"$HOST_CC\" CC=\"$CC\""
;;
*-android*)
CFLAGS="$ANDROID_CFLAGS"
CPPFLAGS="$ANDROID_CPPFLAGS"
LDFLAGS="$ANDROID_LDFLAGS"
export AS CC CXX CPP LD AR RANLIB STRIP CPPFLAGS CFLAGS LDFLAGS
ac_configure_args="$ac_configure_args --build=$build --host=${target_cpu}-${target_os} HOST_CC=\"$HOST_CC\""
;;
*)
ac_configure_args="$ac_configure_args --build=$build --host=$target HOST_CC=\"$HOST_CC\" CC=\"$CC\""
;;
esac
fi
if test "$_MSC_VER"; then
# Use a wrapper script for cl and ml that looks more like gcc.
# autotools can't quite handle an MSVC build environment yet.
ac_configure_args="$ac_configure_args LD=link CPP=\"cl -nologo -EP\" SHELL=sh.exe"
case "${target_cpu}" in
x86_64)
# Need target since MSYS tools into mozilla-build may be 32bit
ac_configure_args="$ac_configure_args CC=\"$_topsrcdir/ctypes/libffi/msvcc.sh -m64\" --build=$build --host=$target"
;;
*)
ac_configure_args="$ac_configure_args CC=$_topsrcdir/ctypes/libffi/msvcc.sh"
;;
esac
fi
if test "$SOLARIS_SUNPRO_CC"; then
# Always use gcc for libffi on Solaris
if test ! "$HAVE_64BIT_OS"; then
ac_configure_args="$ac_configure_args CC=gcc CFLAGS=-m32"
else
ac_configure_args="$ac_configure_args CC=gcc CFLAGS=-m64"
fi
fi
if test "$OS_ARCH" = "OS2"; then
ac_configure_args="$ac_configure_args CFLAGS=-Zomf AR=emxomfar"
fi
if test -n "$MOZ_THUMB2"; then
ac_configure_args="$ac_configure_args --enable-thumb2"
fi
# Use a separate cache file for libffi, since it does things differently
# from our configure.
mkdir -p $_objdir/ctypes/libffi
old_cache_file=$cache_file
cache_file=$_objdir/ctypes/libffi/config.cache
old_config_files=$CONFIG_FILES
unset CONFIG_FILES
AC_OUTPUT_SUBDIRS(ctypes/libffi)
cache_file=$old_cache_file
ac_configure_args="$_SUBDIR_CONFIG_ARGS"
CONFIG_FILES=$old_config_files
fi</pre>
<br /><br />リリース版は<br /><code>./configure --disable-shared --enable-static CC=./msvcc.sh LD=link CPP="cl -nologo -EP"</code><br />デバッグ版は<br /><code>./configure --disable-shared --enable-static --enable-debug CC=./msvcc.sh LD=link CPP="cl -nologo -EP"</code></li>
<br />
<li>ここでmakeしようとすると以下のエラーに遭遇する:<br /><blockquote>
<code>makelibtool: compile: ./msvcc.sh -DHAVE_CONFIG_H -I. -I. -I./include -Iinclude -I./<br />src -Wall -g -fexceptions -O2 -c src\\debug.c -DDLL_EXPORT -DPIC<br />cl -MD -nologo -W3 -DHAVE_CONFIG_H -I. -I. -I./include -Iinclude -I./src -Zi -DE<br />BUG -O2 -OPT:REF -OPT:ICF -INCREMENTAL:NO -c src\debug.c -DDLL_EXPORT -DPIC<br />cl : コマンド ライン warning D9002 : 不明なオプション '-OP' を無視します<br />cl : コマンド ライン warning D9002 : 不明なオプション '-OT' を無視します<br />cl : コマンド ライン warning D9002 : 不明なオプション '-O:' を無視します<br />cl : コマンド ライン warning D9002 : 不明なオプション '-OR' を無視します<br />cl : コマンド ライン warning D9002 : 不明なオプション '-OE' を無視します<br />cl : コマンド ライン warning D9002 : 不明なオプション '-OF' を無視します<br />cl : コマンド ライン warning D9002 : 不明なオプション '-OP' を無視します<br />cl : コマンド ライン warning D9002 : 不明なオプション '-OT' を無視します<br />cl : コマンド ライン warning D9002 : 不明なオプション '-O:' を無視します<br />cl : コマンド ライン warning D9002 : 不明なオプション '-OI' を無視します<br />cl : コマンド ライン warning D9002 : 不明なオプション '-OC' を無視します<br />cl : コマンド ライン warning D9002 : 不明なオプション '-OF' を無視します<br />srcdebug.c<br />c1 : fatal error C1083: ソース ファイルを開けません。'srcdebug.c': No such file or directory</code></blockquote>
どうやらsrc\debug.cのバックスラッシュがおかしいというのがわかるので、libtool内の<br /><br /><pre class="brush: bash">
# Fix the shell variable $srcfile for the compiler.
fix_srcfile_path="\`cygpath -w \"\$srcfile\"\`"</pre>
<br /><br />の-wを-uに変更</li>
<br />
<li>ここでmakeしようとすると今度は以下のエラーに遭遇する:<br /><blockquote>
<code>E:\program\src\libffi\libffi-github\include\ffitarget.h : warning C4819: ファイ<br />ルは、現在のコード ページ (932) で表示できない文字を含んでいます。データの損失を<br />防ぐために、ファイルを Unicode 形式で保存してください。<br />E:\program\src\libffi\libffi-github\include\ffitarget.h(1) : error C2059: 構文エ<br />ラー : '!'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(91) : error C2061: 構文エラー : 識別子 'size_t'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(92) : error C2061: 構文エラー : 識別子 'ValidBytesConst'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(92) : error C2059: 構文エラー : ';'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(97) : error C2061: 構文エラー : 識別子 'WritableElementsConst'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(97) : error C2059: 構文エラー : ';'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(98) : error C2061: 構文エラー : 識別子 'WritableBytesConst'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(98) : error C2059: 構文エラー : ';'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(103) : error C2061: 構文エラー : 識別子 'ElementSizeConst'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(103) : error C2059: 構文エラー : ';'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(107) : error C2059: 構文エラー : '}'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(122) : error C2061: 構文エラー : 識別子 'size_t'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(123) : error C2061: 構文エラー : 識別子 'ValidBytesConst'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(123) : error C2059: 構文エラー : ';'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(128) : error C2061: 構文エラー : 識別子 'WritableElementsConst'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(128) : error C2059: 構文エラー : ';'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(129) : error C2061: 構文エラー : 識別子 'WritableBytesConst'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(129) : error C2059: 構文エラー : ';'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(134) : error C2061: 構文エラー : 識別子 'ElementSizeConst'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(134) : error C2059: 構文エラー : ';'<br />c:\program files (x86)\microsoft visual studio 10.0\vc\include\codeanalysis\sour<br />ceannotations.h(139) : error C2059: 構文エラー : '}'<br />C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\INCLUDE\crtdefs.h(409) :<br />error C2061: 構文エラー : 識別子 'rsize_t'<br />C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\INCLUDE\crtdefs.h(409) :<br />error C2059: 構文エラー : ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(97) : error C2016: C では、構<br />造体または共用体に少なくとも 1 つのメンバーが必要です。<br />E:\program\src\libffi\libffi-github\include\ffi.h(97) : error C2061: 構文エラー<br />: 識別子 'size_t'<br />E:\program\src\libffi\libffi-github\include\ffi.h(101) : error C2059: 構文エラー<br />: '}'<br />E:\program\src\libffi\libffi-github\include\ffi.h(153) : error C2061: 構文エラー<br />: 識別子 'ffi_type_void'<br />E:\program\src\libffi\libffi-github\include\ffi.h(153) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(154) : error C2061: 構文エラー<br />: 識別子 'ffi_type_uint8'<br />E:\program\src\libffi\libffi-github\include\ffi.h(154) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(155) : error C2061: 構文エラー<br />: 識別子 'ffi_type_sint8'<br />E:\program\src\libffi\libffi-github\include\ffi.h(155) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(156) : error C2061: 構文エラー<br />: 識別子 'ffi_type_uint16'<br />E:\program\src\libffi\libffi-github\include\ffi.h(156) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(157) : error C2061: 構文エラー<br />: 識別子 'ffi_type_sint16'<br />E:\program\src\libffi\libffi-github\include\ffi.h(157) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(158) : error C2061: 構文エラー<br />: 識別子 'ffi_type_uint32'<br />E:\program\src\libffi\libffi-github\include\ffi.h(158) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(159) : error C2061: 構文エラー<br />: 識別子 'ffi_type_sint32'<br />E:\program\src\libffi\libffi-github\include\ffi.h(159) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(160) : error C2061: 構文エラー<br />: 識別子 'ffi_type_uint64'<br />E:\program\src\libffi\libffi-github\include\ffi.h(160) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(161) : error C2061: 構文エラー<br />: 識別子 'ffi_type_sint64'<br />E:\program\src\libffi\libffi-github\include\ffi.h(161) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(162) : error C2061: 構文エラー<br />: 識別子 'ffi_type_float'<br />E:\program\src\libffi\libffi-github\include\ffi.h(162) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(163) : error C2061: 構文エラー<br />: 識別子 'ffi_type_double'<br />E:\program\src\libffi\libffi-github\include\ffi.h(163) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(164) : error C2061: 構文エラー<br />: 識別子 'ffi_type_pointer'<br />E:\program\src\libffi\libffi-github\include\ffi.h(164) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(182) : error C2016: C では、構<br />造体または共用体に少なくとも 1 つのメンバーが必要です。<br />E:\program\src\libffi\libffi-github\include\ffi.h(182) : error C2061: 構文エラー<br />: 識別子 'ffi_abi'<br />E:\program\src\libffi\libffi-github\include\ffi.h(184) : error C2143: 構文エラー<br />: '{' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(185) : error C2143: 構文エラー<br />: '{' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(191) : error C2059: 構文エラー<br />: '}'<br />E:\program\src\libffi\libffi-github\include\ffi.h(208) : error C2016: C では、構<br />造体または共用体に少なくとも 1 つのメンバーが必要です。<br />E:\program\src\libffi\libffi-github\include\ffi.h(208) : error C2061: 構文エラー<br />: 識別子 'ffi_sarg'<br />E:\program\src\libffi\libffi-github\include\ffi.h(209) : error C2061: 構文エラー<br />: 識別子 'uint'<br />E:\program\src\libffi\libffi-github\include\ffi.h(209) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(213) : error C2059: 構文エラー<br />: '}'<br />E:\program\src\libffi\libffi-github\include\ffi.h(226) : error C2061: 構文エラー<br />: 識別子 'ffi_java_raw'<br />E:\program\src\libffi\libffi-github\include\ffi.h(226) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(230) : error C2143: 構文エラー<br />: ')' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(230) : error C2143: 構文エラー<br />: '{' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(231) : error C2059: 構文エラー<br />: '型'<br />E:\program\src\libffi\libffi-github\include\ffi.h(235) : error C2143: 構文エラー<br />: ')' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(235) : error C2143: 構文エラー<br />: '{' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(235) : error C2059: 構文エラー<br />: '型'<br />E:\program\src\libffi\libffi-github\include\ffi.h(235) : error C2059: 構文エラー<br />: ')'<br />E:\program\src\libffi\libffi-github\include\ffi.h(236) : error C2143: 構文エラー<br />: ')' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(236) : error C2143: 構文エラー<br />: '{' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(236) : error C2143: 構文エラー<br />: ';' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(236) : error C2059: 構文エラー<br />: '型'<br />E:\program\src\libffi\libffi-github\include\ffi.h(236) : error C2059: 構文エラー<br />: ')'<br />E:\program\src\libffi\libffi-github\include\ffi.h(237) : error C2061: 構文エラー<br />: 識別子 'ffi_raw_size'<br />E:\program\src\libffi\libffi-github\include\ffi.h(237) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(237) : error C2143: 構文エラー<br />: ')' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(237) : error C2143: 構文エラー<br />: '{' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(237) : error C2059: 構文エラー<br />: ')'<br />E:\program\src\libffi\libffi-github\include\ffi.h(243) : error C2143: 構文エラー<br />: ')' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(243) : error C2143: 構文エラー<br />: '{' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(244) : error C2059: 構文エラー<br />: '型'<br />E:\program\src\libffi\libffi-github\include\ffi.h(248) : error C2143: 構文エラー<br />: ')' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(248) : error C2143: 構文エラー<br />: '{' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(248) : error C2059: 構文エラー<br />: '型'<br />E:\program\src\libffi\libffi-github\include\ffi.h(248) : error C2059: 構文エラー<br />: ')'<br />E:\program\src\libffi\libffi-github\include\ffi.h(249) : error C2143: 構文エラー<br />: ')' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(249) : error C2143: 構文エラー<br />: '{' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(249) : error C2143: 構文エラー<br />: ';' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(249) : error C2059: 構文エラー<br />: '型'<br />E:\program\src\libffi\libffi-github\include\ffi.h(249) : error C2059: 構文エラー<br />: ')'<br />E:\program\src\libffi\libffi-github\include\ffi.h(250) : error C2061: 構文エラー<br />: 識別子 'ffi_java_raw_size'<br />E:\program\src\libffi\libffi-github\include\ffi.h(250) : error C2059: 構文エラー<br />: ';'<br />E:\program\src\libffi\libffi-github\include\ffi.h(250) : error C2143: 構文エラー<br />: ')' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(250) : error C2143: 構文エラー<br />: '{' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(250) : error C2059: 構文エラー<br />: ')'<br />E:\program\src\libffi\libffi-github\include\ffi.h(358) : error C2143: 構文エラー<br />: ')' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(358) : error C2143: 構文エラー<br />: '{' が '*' の前にありません。<br />E:\program\src\libffi\libffi-github\include\ffi.h(359) : error C2146: 構文エラー<br />: ';' が、識別子 'abi' の前に必要です。<br />E:\program\src\libffi\libffi-github\include\ffi.h(360) : error C2059: 構文エラー<br />: '型'<br />E:\program\src\libffi\libffi-github\include\ffi.h(362) : error C2059: 構文エラー<br />: ')'<br />E:\program\src\libffi\libffi-github\include\ffi.h(362) : fatal error C1003: プロ<br />グラム内のエラーが 100 個を超えました。コンパイルは中断されます。<br />make[2]: *** [src/debug.lo] Error 1<br />make[2]: Leaving directory `/cygdrive/e/program/src/libffi/libffi-github'<br />make[1]: *** [all-recursive] Error 1<br />make[1]: Leaving directory `/cygdrive/e/program/src/libffi/libffi-github'<br />make: *** [all] Error 2</code></blockquote>
<br />include/ffitarget.hのsymlinkが機能していないことによるエラーなので、src/x86/ffitarget.hをinclude/ffitarget.hに上書きコピーする。</li>
<br />
<li>再度make。.libsにlibffi.libスタティックリンクライブラリができる。</li>
</ol>
<br />
libffiを使うプロジェクトのリンカーインプットにlibffi.libを指定してやればビルドが通る。libffiの利用例は、testsuite内のコードを参照すればよい。<br />
<br />
EnumWindows APIからclosureを呼び出すサンプルコードは以下。<br />
<br />
<pre class="brush: cpp">
#include <windows.h>
#include <stdio.h>
#include <ffi.h>
static void closure_stub(ffi_cif* cif, void* resp, void** args, void* userdata)
{
++(*((DWORD*)userdata));
wchar_t buf[256];
HWND hwnd = *(HWND*)(args[0]);
if (::GetWindowTextW(hwnd, buf, 256) != 0 && ::IsWindowVisible(hwnd))
{
::wprintf_s(L"HWND: 0x%08x - %s\r\n", DWORD(hwnd), buf);
}
*(BOOL*)resp = TRUE;
}
int _tmain(int argc, _TCHAR* argv[])
{
::setlocale(LC_ALL, "Japanese");
void* code;
ffi_closure* pcl = (ffi_closure*)ffi_closure_alloc(sizeof(ffi_closure), &code);
ffi_type* cl_arg_types[2];
cl_arg_types[0] = &ffi_type_pointer;
cl_arg_types[1] = &ffi_type_pointer;
ffi_cif cif;
_ASSERTE(ffi_prep_cif(&cif, FFI_STDCALL, 2, &ffi_type_uint32, cl_arg_types) == FFI_OK);
static DWORD dwInvokeCount = 0;
_ASSERTE(ffi_prep_closure_loc(pcl, &cif, closure_stub, (void*)&dwInvokeCount, code) == FFI_OK);
_ASSERTE(::EnumWindows((WNDENUMPROC)code, NULL) != 0);
::wprintf_s(L"Closure - invoked %d times\r\n", dwInvokeCount);
ffi_closure_free(pcl);
return 0;
}
</pre>
久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-78769645520764204862010-07-11T11:59:00.000+09:002015-02-26T01:52:55.974+09:00新BitTorrentプロトコルµTPを実装するlibutpソースコードの概観5月21日に、<a href="http://github.com/bittorrent/libutp" target="_blank">libutp</a>ソースコードがMITライセンスで<a href="http://forum.utorrent.com/viewtopic.php?id=76640" target="_blank">公開された</a>。libutpとは、<a href="http://www.utorrent.com/documentation/utp" target="_blank">µTP</a> (<a href="http://en.wikipedia.org/wiki/Micro_Transport_Protocol" target="_blank">Micro Transport Protocol</a>)と呼ばれるプロトコルの実装ライブラリである(BitTorrent.orgでは<a href="http://bittorrent.org/beps/bep_0029.html" target="_blank">BEP 29</a>として提案され、IETFでは<a href="http://datatracker.ietf.org/wg/ledbat/charter/" target="_blank">LEDBAT</a>として提案された)。今回は、そのコードを見てみたい。<br/><br/>libutpを公開したのは<a href="http://www.bittorrent.com/" target="_blank">BitTorrent, Inc.</a>で、彼らが配布するBitTorrentクライアント<a href="http://www.utorrent.com/" target="_blank">μTorrent</a>のバージョン1.8 betaがµTPを最初に実装した。BitTorrent, Inc.が元々持っていたオリジナルのBitTorrent実装(Mainline)は、Pythonで書かれたソフトウェアとして有名だが、μTorrentはC++で実装されたWindows用ソフトウェアである。μTorrentはスウェーデン人の<a href="http://en.wikipedia.org/wiki/Ludvig_Strigeus" target="_blank">Ludvig Strigeus</a>氏が開発したが、2006年にBitTorrent, Incに買収されている。μTorrentが登場したとき、備えている機能に比して、GUIアプリにしては(パックされている可能性を勘案しても)きわめて小さいバイナリサイズに感心した。プログラマの力量を誇示するかのようなコンパクトで尖ったソフトウェアだったので、後日の買収のニュースには、落ち着くところに落ち着いたなと感じたのを記憶している。<br/><br/>従来のBitTorrentによるファイル転送がTCPを経由して行われていたのに対し、µTPは、UDP上に構築されたプロトコルを用いる。既にDHTやトラッカーのプロトコルには<a href="http://en.wikipedia.org/wiki/UDP_tracker" target="_blank">UDPが利用されていた</a>(ただしμTorrentによる実装はかなり後になった)が、µTPは、トランスポートプロトコルとしてもUDP上に構築した独自プロトコルを利用する。<br/><br/>BitTorrentクライアントが実装した<a href="http://en.wikipedia.org/wiki/Distributed_hash_table" target="_blank">DHT</a>(分散ハッシュテーブル)は、<a href="http://en.wikipedia.org/wiki/Kademlia" target="_blank">Kademlia</a>というアルゴリズムの実装で、トラッカーに依存しないリソース探索が可能となった。Gnutellaライクな、しかしより構造化された分散キー管理により、トラッカーがダウンしていても目的のリソースを保持するピアのリストを得られる。ネットワークの単一障害点を無くすという建前だが、違法にアップロードされたデータを配布するトラッカーが検挙されつつあった情勢に対抗するものという見方もあった。<br/><br/>今回のµTP実装は、ネットワークの反応が悪くなってきた場合に、自動的に他のトラフィックに道を譲ることで輻輳を防ぐのが目的とされている。オンラインゲームなどのリアルタイム性が重視されるアプリケーションでは一定のレイテンシ以下にping値が収まっていることが要求されるが、TCPのFIFOキューではサイズの大きいP2Pパケットが転送される場合にジッターが発生しやすくサービスの品質に影響を及ぼす。現在のTCPでは、輻輳制御アルゴリズムとして、<a href="http://en.wikipedia.org/wiki/CUBIC_TCP" target="_blank">CUBIC TCP</a>(Linux)、<a href="http://en.wikipedia.org/wiki/Compound_TCP" target="_blank">Compound TCP</a>(Windows)が用いられている。µTPは、BitTorrentのアップロード時に起こりやすい輻輳を特に回避するという目的に適う制御アルゴリズムを、UDP上で実装したものということになる。現在、P2Pアプリケーションを対象としてISPによるトラフィックシェイピングが行われているが、これに対しての回答ともいえるだろう。<br/><br/>前置きは以上で、以下コードを見ていく。libutpは、Windows用のdllとして公開されており、VC9用のソリューションが添付されている。<a href="http://github.com/bittorrent/libutp/blob/master/utp.def" target="_blank">utp.def</a>に公開されているCのAPI関数は以下である。<a name='more'></a><pre class="brush: cpp"><br/> UTP_Create @1<br/> UTP_SetCallbacks @2<br/> UTP_SetSockopt @3<br/> UTP_Connect @4<br/> UTP_IsIncomingUTP @5<br/> UTP_HandleICMP @6<br/> UTP_Write @7<br/> UTP_RBDrained @8<br/> UTP_CheckTimeouts @9<br/> UTP_GetPeerName @10<br/> UTP_GetDelays @11<br/> UTP_Close @12<br/> inet_ntop @13<br/> inet_pton @14<br/></pre><br/><br/>このうち<code>inet_ntop</code>/<code>inet_pton</code>は、Windows VistaからWinsockに実装されているので、古いバージョンのWindows用の実装である。<br/><br/>とりあえずは、µTP接続を生成すると思われる<a href="http://github.com/bittorrent/libutp/blob/master/utp.cpp" target="_blank"><code>UTP_Create</code></a>を見てみよう。<br/><br/><pre class="brush: cpp"><br/>// Create a UTP socket<br/>UTPSocket *UTP_Create(SendToProc *send_to_proc, void *send_to_userdata, const struct sockaddr *addr, socklen_t addrlen)<br/>{<br/> UTPSocket *conn = (UTPSocket*)calloc(1, sizeof(UTPSocket));<br/><br/> g_current_ms = UTP_GetMilliseconds();<br/><br/> UTP_SetCallbacks(conn, NULL, NULL);<br/> conn->our_hist.clear();<br/> conn->their_hist.clear();<br/> conn->rto = 3000;<br/> conn->rtt_var = 800;<br/> conn->seq_nr = 1;<br/> conn->ack_nr = 0;<br/> conn->max_window_user = 255 * PACKET_SIZE;<br/> conn->addr = PackedSockAddr((const SOCKADDR_STORAGE*)addr, addrlen);<br/> conn->send_to_proc = send_to_proc;<br/> conn->send_to_userdata = send_to_userdata;<br/> conn->ack_time = g_current_ms + 0x70000000;<br/> conn->last_got_packet = g_current_ms;<br/> conn->last_sent_packet = g_current_ms;<br/> conn->last_measured_delay = g_current_ms + 0x70000000;<br/> conn->last_rwin_decay = int32(g_current_ms) - MAX_WINDOW_DECAY;<br/> conn->last_send_quota = g_current_ms;<br/> conn->send_quota = PACKET_SIZE * 100;<br/> conn->cur_window_packets = 0;<br/> conn->fast_resend_seq_nr = conn->seq_nr;<br/><br/> // default to version 1<br/> UTP_SetSockopt(conn, SO_UTPVERSION, 1);<br/><br/> // we need to fit one packet in the window<br/> // when we start the connection<br/> conn->max_window = conn->get_packet_size();<br/> conn->state = CS_IDLE;<br/><br/> conn->outbuf.mask = 15;<br/> conn->inbuf.mask = 15;<br/><br/> conn->outbuf.elements = (void**)calloc(16, sizeof(void*));<br/> conn->inbuf.elements = (void**)calloc(16, sizeof(void*));<br/><br/> conn->idx = g_utp_sockets.Append(conn);<br/><br/> LOG_UTPV("0x%08x: UTP_Create", conn);<br/><br/> return conn;<br/>}<br/></pre><br/><br/><pre class="brush: cpp"><br/>// The uTP socket layer calls this to send UDP packets<br/>typedef void SendToProc(void *userdata, const byte *p, size_t len, const struct sockaddr *to, socklen_t tolen);<br/></pre><br/><code>SendToProc</code>は、ユーザー定義のパケット送信関数で、UDPなので<a href="http://msdn.microsoft.com/en-us/library/ms740148.aspx" target="_blank"><code>sendto</code></a>を呼ぶことになるだろう。対象のソケットは、そのユーザー定義関数からアクセスできるところにあらかじめ開いておかなければならない。コネクションレスのUDPなのでソケットは使い回すことになる。対して、ここで作成している<code>UTPSocket</code>オブジェクトは仮想的なソケットで、TCP風に対象アドレス毎に作成して<code>g_utp_sockets</code>グローバル配列に追加する。<br/><br/>ライブラリで利用する順序上は<code>UTP_Connect</code>を見るべきだが、単にSYNパケットを作成して送信するだけのAPIなので、ここで次に注目すべきはパケット受信時に呼ぶAPIである<a href="http://github.com/bittorrent/libutp/blob/master/utp.h" target="_blank"><code>UTP_IsIncomingUTP</code></a>だ。<br/><br/><pre class="brush: cpp"><br/>// Process a UDP packet from the network. This will process a packet for an existing connection,<br/>// or create a new connection and call incoming_proc. Returns true if the packet was processed<br/>// in some way, false if the packet did not appear to be uTP.<br/>bool UTP_IsIncomingUTP(UTPGotIncomingConnection *incoming_proc,<br/> SendToProc *send_to_proc, void *send_to_userdata,<br/> const byte *buffer, size_t len, const struct sockaddr *to, socklen_t tolen);<br/></pre><br/><br/>ネットワークから受け取ったデータをまずこのAPIに読ませ、µTPのパケットかどうかを判定する。<br/><br/><pre class="brush: cpp"><br/>bool UTP_IsIncomingUTP(UTPGotIncomingConnection *incoming_proc,<br/> SendToProc *send_to_proc, void *send_to_userdata,<br/> const byte *buffer, size_t len, const struct sockaddr *to, socklen_t tolen)<br/>{<br/> const PackedSockAddr addr((const SOCKADDR_STORAGE*)to, tolen);<br/><br/> if (len < sizeof(PacketFormat) && len < sizeof(PacketFormatV1)) {<br/> LOG_UTPV("recv %s len:%u too small", addrfmt(addr, addrbuf), (uint)len);<br/> return false;<br/> }<br/><br/> const PacketFormat* p = (PacketFormat*)buffer;<br/> const PacketFormatV1* p1 = (PacketFormatV1*)buffer;<br/><br/> const byte version = UTP_IsV1(p1);<br/> const uint32 id = (version == 0) ? p->connid : uint32(p1->connid);<br/><br/> if (version == 0 && len < sizeof(PacketFormat)) {<br/> LOG_UTPV("recv %s len:%u version:%u too small", addrfmt(addr, addrbuf), (uint)len, version);<br/> return false;<br/> }<br/></pre><br/><br/>生データ<code>buffer</code>を<code>PacketFormat</code>にキャストして、パケットの中身を見ていく。<br/><br/><pre class="brush: cpp"><br/> for (size_t i = 0; i < g_utp_sockets.GetCount(); i++) {<br/> UTPSocket *conn = g_utp_sockets[i];<br/> //LOG_UTPV("Examining UTPSocket %s for %s and (seed:%u s:%u r:%u) for %u",<br/> // addrfmt(conn->addr, addrbuf), addrfmt(addr, addrbuf2), conn->conn_seed, conn->conn_id_send, conn->conn_id_recv, id);<br/> if (conn->addr != addr)<br/> continue;<br/><br/> if (flags == ST_RESET && (conn->conn_id_send == id || conn->conn_id_recv == id)) {<br/> LOG_UTPV("0x%08x: recv RST for existing connection", conn);<br/> if (!conn->userdata || conn->state == CS_FIN_SENT) {<br/> conn->state = CS_DESTROY;<br/> } else {<br/> conn->state = CS_RESET;<br/> }<br/> if (conn->userdata) {<br/> conn->func.on_overhead(conn->userdata, false, len + conn->get_udp_overhead(),<br/> close_overhead);<br/> const int err = conn->state == CS_SYN_SENT ?<br/> ECONNREFUSED :<br/> ECONNRESET;<br/> conn->func.on_error(conn->userdata, err);<br/> }<br/> return true;<br/> } else if (flags != ST_SYN && conn->conn_id_recv == id) {<br/> LOG_UTPV("0x%08x: recv processing", conn);<br/> const size_t read = UTP_ProcessIncoming(conn, buffer, len);<br/> if (conn->userdata) {<br/> conn->func.on_overhead(conn->userdata, false,<br/> (len - read) + conn->get_udp_overhead(),<br/> header_overhead);<br/> }<br/> return true;<br/> }<br/> }<br/></pre><br/><br/>UTPSocketの接続先を比較して、受信パケットが既に管理中のUTPSocketに属する物であれば、ここで処理される。SYNパケット(ST_SYN)でない場合は、送信データの内容を受け取ったことになるので、<code>UTP_ProcessIncoming</code>関数で処理する。その後RSTパケットの処理が続いてから、最後はSYNパケットを受け取って、ピアからの接続要求を処理する。<br/><br/><pre class="brush: cpp"><br/> if (incoming_proc) {<br/> LOG_UTPV("Incoming connection from %s uTP version:%u", addrfmt(addr, addrbuf), version);<br/><br/> // Create a new UTP socket to handle this new connection<br/> UTPSocket *conn = UTP_Create(send_to_proc, send_to_userdata, to, tolen);<br/> // Need to track this value to be able to detect duplicate CONNECTs<br/> conn->conn_seed = id;<br/> // This is value that identifies this connection for them.<br/> conn->conn_id_send = id;<br/> // This is value that identifies this connection for us.<br/> conn->conn_id_recv = id+1;<br/> conn->ack_nr = seq_nr;<br/> conn->seq_nr = UTP_Random();<br/> conn->fast_resend_seq_nr = conn->seq_nr;<br/><br/> UTP_SetSockopt(conn, SO_UTPVERSION, version);<br/> conn->state = CS_CONNECTED;<br/><br/> const size_t read = UTP_ProcessIncoming(conn, buffer, len, true);<br/><br/> LOG_UTPV("0x%08x: recv send connect ACK", conn);<br/> conn->send_ack(true);<br/><br/> incoming_proc(send_to_userdata, conn);<br/><br/> // we report overhead after incoming_proc, because the callbacks are setup now<br/> if (conn->userdata) {<br/> // SYN<br/> conn->func.on_overhead(conn->userdata, false, (len - read) + conn->get_udp_overhead(),<br/> header_overhead);<br/> // SYNACK<br/> conn->func.on_overhead(conn->userdata, true, conn->get_overhead(),<br/> ack_overhead);<br/> }<br/> }<br/></pre><br/><br/><code>incoming_proc</code>はアプリケーション定義コールバックで、ACK送信後に呼ばれる。<br/><br/>それでは、<code>UTP_ProcessIncoming</code>関数を見てみよう。<br/><br/><pre class="brush: cpp"><br/>// Process an incoming packet<br/>// syn is true if this is the first packet received. It will cut off parsing<br/>// as soon as the header is done<br/>size_t UTP_ProcessIncoming(UTPSocket *conn, const byte *packet, size_t len, bool syn = false)<br/>{<br/> UTP_RegisterRecvPacket(conn, len);<br/><br/> g_current_ms = UTP_GetMilliseconds();<br/><br/> conn->update_send_quota();<br/><br/> const PacketFormat *pf = (PacketFormat*)packet;<br/> const PacketFormatV1 *pf1 = (PacketFormatV1*)packet;<br/> const byte *packet_end = packet + len;<br/><br/> // snip<br/><br/> // mark receipt time<br/> uint64 time = UTP_GetMicroseconds();<br/><br/> // snip<br/><br/> uint64 p;<br/><br/> if (conn->version == 0) {<br/> p = uint64(pf->tv_sec) * 1000000 + pf->tv_usec;<br/> } else {<br/> p = pf1->tv_usec;<br/> }<br/><br/> conn->last_measured_delay = g_current_ms;<br/><br/> // get delay in both directions<br/> // record the delay to report back<br/> const uint32 their_delay = (uint32)(p == 0 ? 0 : time - p);<br/> conn->reply_micro = their_delay;<br/> uint32 prev_delay_base = conn->their_hist.delay_base;<br/> if (their_delay != 0) conn->their_hist.add_sample(their_delay);<br/><br/> // if their new delay base is less than their previous one<br/> // we should shift our delay base in the other direction in order<br/> // to take the clock skew into account<br/> if (prev_delay_base != 0 &&<br/> wrapping_compare_less(conn->their_hist.delay_base, prev_delay_base)) {<br/> // never adjust more than 10 milliseconds<br/> if (prev_delay_base - conn->their_hist.delay_base <= 10000) {<br/> conn->our_hist.shift(prev_delay_base - conn->their_hist.delay_base);<br/> }<br/> }<br/><br/> const uint32 actual_delay = conn->version==0<br/> ?(pf->reply_micro==INT_MAX?0:(uint)pf->reply_micro)<br/> :(uint)pf1->reply_micro;<br/><br/> assert(conn->our_hist.get_value() >= 0);<br/> // if the actual delay is 0, it means the other end<br/> // hasn't received a sample from us yet, and doesn't<br/> // know what it is. We can't update out history unless<br/> // we have a true measured sample<br/> prev_delay_base = conn->our_hist.delay_base;<br/> if (actual_delay != 0) conn->our_hist.add_sample(actual_delay);<br/> assert(conn->our_hist.get_value() >= 0);<br/><br/> // if our new delay base is less than our previous one<br/> // we should shift the other end's delay base in the other<br/> // direction in order to take the clock skew into account<br/> // This is commented out because it creates bad interactions<br/> // with our adjustment in the other direction. We don't really<br/> // need our estimates of the other peer to be very accurate<br/> // anyway. The problem with shifting here is that we're more<br/> // likely shift it back later because of a low latency. This<br/> // second shift back would cause us to shift our delay base<br/> // which then get's into a death spiral of shifting delay bases<br/>/* if (prev_delay_base != 0 &&<br/> wrapping_compare_less(conn->our_hist.delay_base, prev_delay_base)) {<br/> // never adjust more than 10 milliseconds<br/> if (prev_delay_base - conn->our_hist.delay_base <= 10000) {<br/> conn->their_hist.Shift(prev_delay_base - conn->our_hist.delay_base);<br/> }<br/> }<br/>*/<br/> // only apply the congestion controller on acks<br/> // if we don't have a delay measurement, there's<br/> // no point in invoking the congestion control<br/> if (actual_delay != 0 && acked_bytes >= 1)<br/> conn->apply_ledbat_ccontrol(acked_bytes, actual_delay, min_rtt);<br/></pre><br/><br/><code>UTP_GetMicroseconds</code>は、<a href="http://msdn.microsoft.com/en-us/library/ms644904.aspx" target="_blank"><code>QueryPerformanceCounter</code></a>を用いて<code>(</code>monotonicになるように修正した上で)マイクロ秒単位の時間を取得する。それと、パケットに記録されている時間との差分を取っている。<br/><br/>ここでわかるのは、アップロードされた送信パケットが実際に着信するまでにかかった時間(<code>their_delay</code>)を<code>reply_micro</code>として保存し、返信して、それを受け取った側で遅延(<code>actual_delay</code>)として解釈しているということだ。つまり、パケットが行って帰ってくるまでのラウンドトリップタイム(RTT)ではなく、アップロード時の遅延を計測して輻輳を検出しようとしている。この後、<code>apply_ledbat_ccontrol</code>関数によって、場合によってはウィンドウサイズを変化させて他のTCP接続を優先することになる。<br/><br/><pre class="brush: cpp"><br/>void UTPSocket::apply_ledbat_ccontrol(size_t bytes_acked, uint32 actual_delay, int64 min_rtt)<br/>{<br/> // the delay can never be greater than the rtt. The min_rtt<br/> // variable is the RTT in microseconds<br/><br/> assert(min_rtt >= 0);<br/> int32 our_delay = min<uint32>(our_hist.get_value(), uint32(min_rtt));<br/> assert(our_delay != INT_MAX);<br/> assert(our_delay >= 0);<br/> assert(our_hist.get_value() >= 0);<br/><br/> SOCKADDR_STORAGE sa = addr.get_sockaddr_storage();<br/> UTP_DelaySample((sockaddr*)&sa, our_delay / 1000);<br/><br/> // This test the connection under heavy load from foreground<br/> // traffic. Pretend that our delays are very high to force the<br/> // connection to use sub-packet size window sizes<br/> //our_delay *= 4;<br/><br/> // target is microseconds<br/> int target = CCONTROL_TARGET;<br/> if (target <= 0) target = 100000;<br/><br/> double off_target = target - our_delay;<br/><br/> // this is the same as:<br/> //<br/> // (min(off_target, target) / target) * (bytes_acked / max_window) * MAX_CWND_INCREASE_BYTES_PER_RTT<br/> //<br/> // so, it's scaling the max increase by the fraction of the window this ack represents, and the fraction<br/> // of the target delay the current delay represents.<br/> // The min() around off_target protects against crazy values of our_delay, which may happen when th<br/> // timestamps wraps, or by just having a malicious peer sending garbage. This caps the increase<br/> // of the window size to MAX_CWND_INCREASE_BYTES_PER_RTT per rtt.<br/> // as for large negative numbers, this direction is already capped at the min packet size further down<br/> // the min around the bytes_acked protects against the case where the window size was recently<br/> // shrunk and the number of acked bytes exceeds that. This is considered no more than one full<br/> // window, in order to keep the gain within sane boundries.<br/><br/> assert(bytes_acked > 0);<br/> double window_factor = (double)min(bytes_acked, max_window) / (double)max(max_window, bytes_acked);<br/> double delay_factor = off_target / target;<br/> double scaled_gain = MAX_CWND_INCREASE_BYTES_PER_RTT * window_factor * delay_factor;<br/><br/> // since MAX_CWND_INCREASE_BYTES_PER_RTT is a cap on how much the window size (max_window)<br/> // may increase per RTT, we may not increase the window size more than that proportional<br/> // to the number of bytes that were acked, so that once one window has been acked (one rtt)<br/> // the increase limit is not exceeded<br/> // the +1. is to allow for floating point imprecision<br/> assert(scaled_gain <= 1. + MAX_CWND_INCREASE_BYTES_PER_RTT * (int)min(bytes_acked, max_window) / (double)max(max_window, bytes_acked));<br/><br/> if (scaled_gain > 0 && g_current_ms - last_maxed_out_window > 300) {<br/> // if it was more than 300 milliseconds since we tried to send a packet<br/> // and stopped because we hit the max window, we're most likely rate<br/> // limited (which prevents us from ever hitting the window size)<br/> // if this is the case, we cannot let the max_window grow indefinitely<br/> scaled_gain = 0;<br/> }<br/><br/> if (scaled_gain + max_window < MIN_WINDOW_SIZE) {<br/> max_window = MIN_WINDOW_SIZE;<br/> } else {<br/> max_window = (size_t)(max_window + scaled_gain);<br/> }<br/><br/> // make sure that the congestion window is below max<br/> // make sure that we don't shrink our window too small<br/> max_window = clamp<size_t>(max_window, MIN_WINDOW_SIZE, opt_sndbuf);<br/></pre><br/><br/><code>CCONTROL_TARGET</code>は今のところ100 * 1000µs = 100msで、100msを下回っていれば目標のレイテンシが達成できているということになる。ターゲットから外れている数値(<code>off_target</code>)に応じて<code>scaled_gain</code>を算出し、100msよりレイテンシが低ければウィンドウサイズ(<code>max_window</code>)を大きくし、100msを超えていたら輻輳とみなしてウィンドウサイズを小さくし、別のアプリケーションに道を譲るという輻輳制御が行われる。<br/><br/>この後、<code>UTP_ProcessIncoming</code>では、Selective ACKという処理でパケットロスを判定する。<br/><br/><pre class="brush: cpp"><br/> // count bytes acked by EACK<br/> if (selack_ptr != NULL) {<br/> acked_bytes += conn->selective_ack_bytes((pk_ack_nr + 2) & ACK_NR_MASK,<br/> selack_ptr, selack_ptr[-1], min_rtt);<br/> }<br/></pre><br/><br/><pre class="brush: cpp"><br/>void UTPSocket::selective_ack(uint base, const byte *mask, byte len)<br/>{<br/> // snip<br/><br/> bool back_off = false;<br/> int i = 0;<br/> while (nr > 0) {<br/> uint v = resends[--nr];<br/> // don't consider the tail of 0:es to be lost packets<br/> // only unacked packets with acked packets after should<br/> // be considered lost<br/> OutgoingPacket *pkt = (OutgoingPacket*)outbuf.get(v);<br/><br/> // this may be an old (re-ordered) packet, and some of the<br/> // packets in here may have been acked already. In which<br/> // case they will not be in the send queue anymore<br/> if (!pkt) continue;<br/><br/> // used in parse_log.py<br/> LOG_UTP("0x%08x: Packet %u lost. Resending", this, v);<br/><br/> // On Loss<br/> back_off = true;<br/>#ifdef _DEBUG<br/> ++_stats._rexmit;<br/>#endif<br/> send_packet(pkt);<br/> fast_resend_seq_nr = v + 1;<br/><br/> // Re-send max 4 packets.<br/> if (++i >= 4) break;<br/> }<br/><br/> if (back_off)<br/> maybe_decay_win();<br/><br/> duplicate_ack = count;<br/>}<br/></pre><br/><br/>同じACKを3回受け取った場合、輻輳が激しくなってパケットロスが起きたとみなし、パケット再送を促すとともに、<code>maybe_decay_win</code>を呼んでウィンドウサイズを縮小する。ここは<a href="http://en.wikipedia.org/wiki/TCP_congestion_avoidance_algorithm" target="_blank">TCP Reno</a>同様である。<br/><br/>µTPの基本的な動作は、以上である。一見する限り、これでμTorrentを起動していてもWebブラウザを含む他のネットワークアプリの動作が重くならなくなるのであれば、万歳といったところだ。ところが、torrentコミュニティの中では、事実上μTorrentが(µTPをサポートする)μTorrentのみをピアとして優先するようになり他のクライアントが差別されかねないことや、µTP適用で絶対的転送速度が下がる可能性があることを危惧して、µTPに対しての批判や慎重論も根強いようである。とはいえ、μTorrentが現在非常に大きなシェアを占めていることは事実であり、µTPを実装するクライアントが増加するのも時間の問題だろう。<a href="http://www.bitcomet.com/" target="_blank">BitComet</a>や<a href="http://www.xunlei.com/" target="_blank">Xunlei</a>といった中国で人気のあるクライアントが追随すれば、さらに趨勢は決定的となる。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-9753712035574351202009-12-14T10:21:00.000+09:002015-02-26T02:03:41.845+09:00Quid Pro QuoOne thing that I don't like about scripting languages is usage of bizarre identifiers. Statically compiled languages don't care about verbose identifiers, but scripting languages often employ abbreviated names to gain run-time performance and typing speed.<br />
<br />
<pre class="brush: perl">my ( $x, $y ) = $self->invoke(
+{
METHOD => 'foo',
PARAM => 'bar',
},
''
);</pre>
<br />
<br />
In this Perl snippet, I had no idea what "+{}" stands for. It looks like some kind of hash, but not sure. Googling it didn't bring good results since Google doesn't recognize a string made only of non-alphabetical characters such as braces and math operators.<br />
<br />
To figure out its meaning I wasted so much time by trying many different words in Google. Anyway here's the answer:<br />
<blockquote>
or to force an anon hash constructor use <code>+{</code>:<br />
<pre><ol>
<li> @hashes = <a href="http://perldoc.perl.org/functions/map.html">map</a> +{ <a href="http://perldoc.perl.org/functions/lc.html">lc</a>($_) => 1 }, @array # EXPR, so needs comma at end</li>
</ol>
</pre>
to get a list of anonymous hashes each with only one entry apiece.</blockquote>
It's embedded in <a href="http://perldoc.perl.org/perlfunc.html" target="_blank">perlfunc</a> and I couldn't find any other references in the official documents.<br />
<br />
I had a hard time with Ruby too when the last argument for a method has an ampersand (&) prepended. In the Japanese version of the Ruby manual, it's obscurely explained in a <a href="http://www.ruby-lang.org/ja/man/html/FAQ_A5D6A5EDA5C3A5AFC9D5A4ADA5E1A5BDA5C3A5C9B8C6A4D3BDD0A4B7.html" target="_blank">single line</a>. It's very hard to find since there's no word for an ampersand in Japanese. It has no English version but The Ruby Programming Wikibooks has a <a href="http://en.wikibooks.org/wiki/Ruby_Programming/Syntax/Method_Calls#The_ampersand_.28.26.29" target="_blank">related part</a> which is a lot better with examples.<br />
<blockquote>
<h4>
The ampersand (&)</h4>
<br />
The ampersand operator can be used to explicitly convert between blocks and Procs in a couple of cases. It is worthy to understand how these work.<br />
<br />
Remember how I said that although an attached block is converted to a Proc under the hood, it is not accessible as a Proc from inside the method ? Well, if an ampersand is prepended to the last argument in the argument list of a method, the block attached to this method is converted to a Proc object and gets assigned to that last argument:<br />
<br />
<pre class="brush: ruby">def contrived(a, &f)
# the block can be accessed through f
f.call(a)
# but yield also works !
yield(a)
end
# this works
contrived(25) {|x| puts x}
# this raises ArgumentError, because &f
# isn't really an argument - it's only there
# to convert a block
contrived(25, lambda {|x| puts x})</pre>
</blockquote>
<br />
Regarding manuals, MSDN is still the best place. Don't know what "=>" means in C#? OK search it in the <a href="http://msdn.microsoft.com/en-us/library/6a71f45d.aspx" target="_blank">C# operators</a>. It's the <a href="http://msdn.microsoft.com/en-us/library/bb311046.aspx" target="_blank">lambda operator</a> introduced in C# 3.0. Since I always code with a language specification on my side when it's not C++, the quality of reference documents can be directly reflected in productivity.久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-8086589128439219422009-09-01T14:50:00.000+09:002015-02-26T02:16:14.966+09:00C++ Dependency Injection (or the next best thing) on Windows<a href="http://en.wikipedia.org/wiki/Dependency_injection" target="_blank">Dependency Injection</a> (DI) is one of the recent buzz phrases within the design pattern community after Martin Fowler picked it up with another related concept <a href="http://en.wikipedia.org/wiki/Inversion_of_control" target="_blank">Inversion of Control</a> (IoC) in 2004. Since then, it became popular in Java lightweight frameworks where adding late binding was seen as a critical step for upping productivity.<br/><br/>The core value of DI is basically represented in these 2 points:<ol><li>Testability. DI makes testing easy by enabling loosely coupled components that are easily replaced with mock objects.</li><li>Dependency control. When you deploy a DI framework instead of using the Factory Method pattern, dependency can be managed outside of components.</li></ol>The first point is rather obvious. To understand the second point, I recommend you to browse the <a href="http://code.google.com/p/google-guice/" target="_blank">Google Giuce</a> lightweight Java DI framework for its video presentation and documents. It illustrates the necessity of such a framework for Java in a straightforward way. Particularly, Giuce appears to be a neat implementation of DI where you don't need to manage XML configuration files unlike other frameworks. Just like inner DSL, dependency is handled in type-safe Java code even though it ditches type-safe factories scattered in code.<br/><br/>But, as Ruby and other dynamic languages gets more attention, the relative mind share of DI seems to get smaller. In Ruby, you can swap methods of instances dynamically at the side of a test class. The dynamic nature of Ruby can enable whole other tricks dependent on it such as ActiveRecord.<br/><br/>So what about DI? Is it just a glorified abstract factory? Unfortunately, there still have to be environments where DI makes things a lot easier. Not just Java, but other static language, such as C++. Since my programming history is MS-Windows-centric, my interest toward DI is its applicability for Windows app development. DI is a lot discussed in the context of Java/.NET enterprise development, but its necessity would be even higher in C++ development on Windows as long as other requirements such as performance permit.<br/><br/><a name='more'></a><br/><br/>If you are a Windows programmer, IoC can't be a novel concept since without it no Windows GUI application can be built. Plugging in your implementation code into some framework is not an alien concept for us. As for DI, its merit of testability is a big plus for a project that includes many C++ components and consumes mind-bogglingly long time to rebuild.<br/><br/>But DI discussion seems thin around here. Why? It seems existing implementations in Java often rely on reflection which is not available in C++. For example, Giuce uses <code>@inject</code> annotation to tell injection points. It may be just for its AOP features, but lacking reflection makes it a lot harder to implement. Using exotic macros and templates can change the situation, but I guess not much. Another issue is, as I mentioned already, it's a form of late binding hence performance hit is expected. If you design it not to get hit hard, it will not be loosely coupled any more. At this point, I almost gave up the idea of DI for Windows C++.<br/><br/>Instead, my interest shifted to finding the next best thing available which can help us to test binary components without hassle. So I searched - and found a solution. It's called <a href="http://msdn.microsoft.com/en-us/library/ms973913.aspx" target="_blank">registration-free COM</a> or Reg-Free COM.<br/><br/>OK don't stone me :) It's a very nice feature or a hidden gem in Windows. It's only available on Windows XP and later, but it won't be a problem. The basic idea behind <a href="http://en.wikipedia.org/wiki/Component_Object_Model" target="_blank">COM (Component Object Model)</a> is a binary interface for reusable components. A part of its concept was later adopted by <a href="http://en.wikipedia.org/wiki/XPCOM" target="_blank">XPCOM</a> for Mozilla Firefox.<br/><br/>One issue that nags developers and users regarding the use of COM components is it requires Registry entry to resolve an interface query. GUIDs of COM components have to be registered with regsvr32.exe in the HKEY_CLASSES_ROOT (HKCR) Registry key. It makes deployment of COM components really cumbersome even though COM itself is a really reasonable technology. Uninstalling them can be a PITA too.<br/><br/>Enter Reg-Free COM, now Registry entry is not necessary to use a COM component as long as XML manifests are provided to load correct components. In normal COM, the ClassFactory and related platform facility searches Registry entry when a specific CLSID or IID is requested in CoCreateInstance or QueryInterface, whereas in Reg-Free COM dependency resolution is done at application startup by parsing manifests. There are 2 kinds of manifests: application manifest and assembly manifest. The former describes on what components an application depends. The latter describes assembly's identity such as a dll file name and CLSID.<br/><br/>A typical application manifest is as follows (lifted from the sample at MSDN)<br/><br/><pre class="brush: xml"><br/><?xml version="1.0" encoding="UTF-8" standalone="yes"?><br/><assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"><br/><assemblyIdentity<br/> type = "win32"<br/> name = "client"<br/> version = "1.0.0.0" /><br/><dependency><br/> <dependentAssembly><br/> <assemblyIdentity<br/> type="win32"<br/> name="SideBySide.X"<br/> version="1.0.0.0" /><br/> </dependentAssembly><br/></dependency><br/></assembly><br/></pre><br/><br/>It shows it depends on an assembly with the name "SideBySide.X". The dll loader tries to find a file with the name "SideBySide.X.manifest" when reading this application manifest. In SideBySide.X.manifest, its correspondent dll is described.<br/><br/><pre class="brush: xml"><br/><?xml version="1.0" encoding="UTF-8" standalone="yes"?><br/><assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"><br/><br/><assemblyIdentity<br/> type="win32"<br/> name="SideBySide.X"<br/> version="1.0.0.0" /><br/><br/><file name = "SideBySide.dll"><br/><br/><comClass<br/> clsid="{4B72FC46-C543-4101-80DB-7777848D1357}"<br/> threadingModel = "Apartment" /><br/><br/><typelib tlbid="{E6A9CD40-8559-4E17-A0D9-C68B038B4FA0}"<br/> version="1.0" helpdir=""/><br/><br/></file><br/><br/><comInterfaceExternalProxyStub<br/> name="ISideBySideClass"<br/> iid="{CBA85B94-9C11-43aa-84F6-30B90145FD3E}"<br/> proxyStubClsid32="{00020424-0000-0000-C000-000000000046}"<br/> baseInterface="{00000000-0000-0000-C000-000000000046}"<br/> tlbid = "{E6A9CD40-8559-4E17-A0D9-C68B038B4FA0}" /><br/><br/></assembly><br/></pre><br/><br/><br/>A <code>comInterfaceExternalProxyStub</code> node is necessary when a component is called in a different kind of a thread apartment from that of the component.<br/><br/><a href="http://en.wikipedia.org/wiki/Dependency_injection" target="_blank">Dependency Injection</a> (DI) is one of the recent buzz phrases within the design pattern community after Martin Fowler picked it up with another related concept <a href="http://en.wikipedia.org/wiki/Inversion_of_control" target="_blank">Inversion of Control</a> (IoC) in 2004. Since then, it became popular in Java lightweight frameworks where adding late binding was seen as a critical step for upping productivity.</p><br/>The core value of DI is basically represented in these 2 points:<br/><ol><br/> <li>Testability. DI makes testing easy by enabling loosely coupled components that are easily replaced with mock objects.</li><br/> <li>Dependency control. When you deploy a DI framework instead of using the Factory Method pattern, dependency can be managed outside of components.</li><br/></ol><br/>The first point is rather obvious. To understand the second point, I recommend you to browse the <a href="http://code.google.com/p/google-guice/" target="_blank">Google Giuce</a> lightweight Java DI framework for its video presentation and documents. It illustrates the necessity of such a framework for Java in a straightforward way. Particularly, Giuce appears to be a neat implementation of DI where you don't need to manage XML configuration files unlike other frameworks. Just like inner DSL, dependency is handled in type-safe Java code even though it ditches type-safe factories scattered in code.<br/><br/>But, as Ruby and other dynamic languages gets more attention, the relative mind share of DI seems to get smaller. In Ruby, you can swap methods of instances dynamically at the side of a test class. The dynamic nature of Ruby can enable whole other tricks dependent on it such as ActiveRecord.<br/><br/>So what about DI? Is it just a glorified abstract factory? Unfortunately, there still have to be environments where DI makes things a lot easier. Not just Java, but other static language, such as C++. Since my programming history is MS-Windows-centric, my interest toward DI is its applicability for Windows app development. DI is a lot discussed in the context of Java/.NET enterprise development, but its necessity would be even higher in C++ development on Windows as long as other requirements such as performance permit.<br/><br/><!--more--><br/><br/>If you are a Windows programmer, IoC can't be a novel concept since without it no Windows GUI application can be built. Plugging in your implementation code into some framework is not an alien concept for us. As for DI, its merit of testability is a big plus for a project that includes many C++ components and consumes mind-bogglingly long time to rebuild.<br/><br/>But DI discussion seems thin around here. Why? It seems existing implementations in Java often rely on reflection which is not available in C++. For example, Giuce uses <code>@inject</code> annotation to tell injection points. It may be just for its AOP features, but lacking reflection makes it a lot harder to implement. Using exotic macros and templates can change the situation, but I guess not much. Another issue is, as I mentioned already, it's a form of late binding hence performance hit is expected. If you design it not to get hit hard, it will not be loosely coupled any more. At this point, I almost gave up the idea of DI for Windows C++.<br/><br/>Instead, my interest shifted to finding the next best thing available which can help us to test binary components without hassle. So I searched - and found a solution. It's called <a href="http://msdn.microsoft.com/en-us/library/ms973913.aspx" target="_blank">registration-free COM</a> or Reg-Free COM.<br/><br/>OK don't stone me :) It's a very nice feature or a hidden gem in Windows. It's only available on Windows XP and later, but it won't be a problem. The basic idea behind <a href="http://en.wikipedia.org/wiki/Component_Object_Model" target="_blank">COM (Component Object Model)</a> is a binary interface for reusable components. A part of its concept was later adopted by <a href="http://en.wikipedia.org/wiki/XPCOM" target="_blank">XPCOM</a> for Mozilla Firefox.<br/><br/>One issue that nags developers and users regarding the use of COM components is it requires Registry entry to resolve an interface query. GUIDs of COM components have to be registered with regsvr32.exe in the HKEY_CLASSES_ROOT (HKCR) Registry key. It makes deployment of COM components really cumbersome even though COM itself is a really reasonable technology. Uninstalling them can be a PITA too.<br/><br/>Enter Reg-Free COM, now Registry entry is not necessary to use a COM component as long as XML manifests are provided to load correct components. In normal COM, the ClassFactory and related platform facility searches Registry entry when a specific CLSID or IID is requested in CoCreateInstance or QueryInterface, whereas in Reg-Free COM dependency resolution is done at application startup by parsing manifests. There are 2 kinds of manifests: application manifest and assembly manifest. The former describes on what components an application depends. The latter describes assembly's identity such as a dll file name and CLSID.<br/><br/>A typical application manifest is as follows (lifted from the sample at MSDN)<br/><br/><pre class="brush: xml"><br/><?xml version="1.0" encoding="UTF-8" standalone="yes"?><br/><assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"><br/><assemblyIdentity<br/> type = "win32"<br/> name = "client"<br/> version = "1.0.0.0" /><br/><dependency><br/> <dependentAssembly><br/> <assemblyIdentity<br/> type="win32"<br/> name="SideBySide.X"<br/> version="1.0.0.0" /><br/> </dependentAssembly><br/></dependency><br/></assembly><br/></pre><br/><br/>It shows it depends on an assembly with the name "SideBySide.X". The dll loader tries to find a file with the name "SideBySide.X.manifest" when reading this application manifest. In SideBySide.X.manifest, its correspondent dll is described.<br/><br/><br/><br/>Back on the topic of DI, the most important point is you can modify dependency by editing these manifests. You can change an application manifest to refer to another dependent manifest. It's possible to change the file name in an assembly manifest to load a different dll that has a different implementation of a coclass with the same CLSID. As long as IID matches you can swap it with a mock object.<br/><br/>Of course there are drawbacks. If you feel making COM objects tedious, it's not for you. But I'd take unit testing without rebuild any day. Also, since it resolves dependency at startup, it can slow down the process a bit if there are too many Reg-Free COM objects. After startup the overhead should not be larger than regular COM usage.<br/><br/>One thing I'd like to emphasize in this article is reinventing the wheel costs you a lot. Instead, first consider using what's available to your liking before jumping the gun to write your own version. Most likely, someone already came up with a decent idea deliberately or indeliberately.
久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-57630475573139647312009-08-19T09:25:00.000+09:002017-10-26T02:26:29.009+09:00Red Pill or Blue Pill先月7月頭に、Googleの<a href="http://googlejapan.blogspot.com/2009/07/google-chrome-os.html" target="_blank" title="Chrome OS">Chrome OS</a>が発表された。Linuxカーネル + 軽量ウィンドウマネージャ + Google Chrome Webブラウザという構成を取るといわれるこのOSは、いわゆるシンクライアントを最低限のハードウェアリソースで実現するための環境となると推測される。ただし、従来の企業向け用途の単なるシンクライアントではなく、ビデオ再生や3Dなど、ハードウェアリソースを要求するリッチメディアがWeb上で当たり前のようにやりとりされている現在や近未来のパーソナルコンピューティング環境をも視野に入れていることは間違いないだろう。<br />
<br />
これが、かつてOracleが提唱した<a href="http://ja.wikipedia.org/wiki/%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF" target="_blank">ネットワークコンピュータ</a>を越えていくものとなるかどうかは、実際のプロダクトの出来を見なければ、占うにはまだ早すぎるといわざるをえない。それでも、Google Chromeに続きChrome OSによって、Googleが考えるところの理想のクライアント環境を実現しようとする動きは、きわめて自然であるし、一貫性があるといえる。しかし、この一貫性は、単にGoogle内部で完結しているものではなく、歴史的な流れの中のある一点で、たまたまGoogleという企業が、マイルストーンとして一般に認知される発表を行うことを可能とするリソースや動機を有していた、という見方もできよう。<br />
<br />
<h2>
test 1</h2>
<br />
この仮想的な歴史をめぐる可能世界で、Chrome OSの前駆者として、またもう一つの可能性として念頭にあるのは、Microsoftの活動だ。今年の2月に発表された<a href="http://research.microsoft.com/apps/pubs/default.aspx?id=79655" target="_blank">Gazelle</a>は、プリンシパルベースのセキュリティという概念を主軸に置いて、Google Chromeより徹底したセキュリティアーキテクチャのWebブラウザを、OSをも巻き込んだ形で実装してしまおうという試みである。このアプローチは、Google Chrome実装の経緯と、興味深い鏡像関係を成している。Google Chromeの実装者らは、Windows上でセキュリティサンドボックスを実現するために、Windows XP SP2の実装に<a href="http://www.hanselman.com/blog/TheWeeklySourceCode33MicrosoftOpenSourceInsideGoogleChrome.aspx" target="_blank">深く依存せざるを得なかった</a>。市場を独占するOSのAPIや実装を管理できるというリソースを持ち合わせるMicrosoftにとっては、そのあたりの苦労は、技術的なものではなく、単に手続き的なものでしかない。そして、Gazelleが理論として存在したとしても、それが実際の製品に生かされるかどうかは、別の次元の話である。<br />
<br />
Gazelleという試みと別の次元で並行して走る現実世界でのMicrosoftの問題は、法的な懸念である。1997年のInternet Explorer 4で、MicrosoftはOSのシェルとWebブラウザの統合を図った。その余波が、WebブラウザなどのコンポーネントをOSに同梱する際に紛糾するというヨーロッパでの今日的状況にまで及んでいる。この、法的な懸念をかわしつつ、MicrosoftがGazelleを実現しようとするならば、必要な機能のほとんどはOS側に盛り込んで、それを自社のWebブラウザからだけではなく他社からも利用できるようにすることになるだろう。Webブラウザというアプリケーションに固有の部分は、<a href="http://ja.wikipedia.org/wiki/%E5%A4%9A%E5%B1%A4%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3" target="_blank">3層アーキテクチャ</a>でいえば、データの表示を司るいわゆるプレゼンテーション層や、Webブラウザ固有の操作に限定されたビジネスロジック層に限ったものとなり、データのやり取りを管理するデータ層は、OSが引き取ることになる。<br />
<br />
<h2>
test 2</h2>
<br />
ここで、Webブラウザの話に戻ると、Google Chromeの構成は、すでにそのような階層を前提としている。データのやり取りや管理を行うカーネルと、データの表示を行うレンダラーの分離は、Gazelle的なOSにおけるOSとアプリケーションの関係にそのままあてはまる。であれば、GoogleもまたOSに向かうのは当然の流れだろう。その意味で、Google Chromeとは、Windows等のOSの中に、Googleの植民地としての仮想OSを構築するという試みであったといえる。そして、仮想OSをOSに変えるには、ハードウェアとの仲立ちを行ってくれる層(たとえばLinuxカーネル)があればよい。<br />
<br />
Webブラウザを仮想OSとしてとらえ、各々の機能を別プロセスに分けサンドボックス化してセキュリティを担保するという構成自体は、Google Chromeが初めてではない。2008年3月に発表されている<a href="http://www.iti.illinois.edu/news/press-releases/iti-researchers-create-secure-web-browser" target="_blank">Opus Palladianum</a>は、まさにそのような実験だった。そこでの課題は、パフォーマンスである。データの流れに対して要所要所で監査が行われるため、そのためのレイテンシ増大が問題となっていた。これに対し、Google Chromeの場合は、機能分割の粒度を下げたり、各々の部品の最適化を進めることで、現実解を導いている。<br />
<br />
いずれにせよ、ネットワークリソースのサンドボックス化、仮想マシン化、そしてクラウド化というトレンドは、最早後戻りのないポイントにまで進展してきている。この現実(<a href="http://en.wikipedia.org/wiki/Redpill" target="_blank">Red Pill</a>)を受け入れ、積極的に生かすことができるか、もしくは幸福な幻想(Blue Pill)のもとに留まるか - 今後数年で、さまざまな選択と、その結果を目にすることになるのは間違いない。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-53028226469353303592009-04-15T16:59:00.000+09:002017-06-14T01:17:31.466+09:00Whisky-Tango-Foxtrot更新を再開しようかと思ったのだが、まだネタがないので仕方なくblog風に先日撮った写真で茶を濁す。<br />
<br />
<table align="center" cellpadding="0" cellspacing="0" class="tr-caption-container" style="margin-left: auto; margin-right: auto; text-align: center;"><tbody>
<tr><td style="text-align: center;"><a href="http://2.bp.blogspot.com/-k_cLy7KEPoU/VO4EskteX9I/AAAAAAAADiA/tFaRwIXc46s/s1600/20090403.png" imageanchor="1" style="margin-left: auto; margin-right: auto;"><img border="0" src="https://2.bp.blogspot.com/-k_cLy7KEPoU/VO4EskteX9I/AAAAAAAADiA/tFaRwIXc46s/s1600/20090403.png" /></a></td></tr>
<tr><td class="tr-caption" style="text-align: center;">芝公園にて、2009年4月3日</td></tr>
</tbody></table>
<br />
昨年も書いているように、サイト自体をCMSかblogツールにいい加減移行したいと本当は思っていて、そのくせ時間がないので踏み切れないでいる。<br />
<br />
VS2008はやっと自分のPCにインストールしたところで、昨年書いた課題に取りかかる以前の段階で1年以上足踏みしてしまっている。その間に生活環境が変化したため、以前描いていた計画も、実装に取りかかる遙か以前の段階で既に再検討を余儀なくされている。具体的には、DICEに関して、ある部分のみC++/CLIを用いて.NET化することを考えていた。それというのも、ネイティブコードもそろそろ潮時かと考えて、C++を利用する部分を徐々に縮小させることを意図していたからであった。現在もその方針自体は誤りではなかったと考えているが、今それを為すべきか、そのための時間があるかということを熟慮すると、同じ時間を使うにしても他にやらなければならないことがあるのではないかというのが今回至った結論である。<br />
<br />
また、去年計画・調査だけ行って結局頓挫したものとして、どのみち日の目を見ないだろうから忘れないうちに書いてしまうと、RTMPをDICEに実装することを考えていた。それというのも、抽象サーバーフレームワークであるDICEのプロトコル実装の試金石として商業的に意味のありそうなプロトコルを是非実装しておきたかったのと、Flashとの親和性を高めておきたかったという事情がある。サーバー用のソフトウェアスタック(と.NETで作った簡易クライアント)しかコントロールできない私にとって、Flash + JavaScriptはクライアントアプリ作成のためのRAD環境たり得た。ところが状況は変わり、C++の非常に有望なクライアント用ソフトウェアスタックが自由なライセンスの元で利用できることになり、今はそれをどう活かすかについて思案している。勘の良い諸兄ならそれが何かもうお分かりだろう。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-65618075299068628052008-02-11T17:33:00.000+09:002015-02-26T02:25:42.932+09:00WindowsにおけるC++へのPHP組み込み環境の構築<a href="https://www.blogger.com/perl_ruby_multithreading_embedding.html" target="_blank">前回の記事</a>では、C/C++コードへのPerlとRubyの組み込みを扱った。<a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a>への組み込みの評価を兼ねていて、当時はPerlを使うことになった。一方で、DICEのWebサーバとしての側面をもっと強調せねばという課題が最近わりと念頭にあり、Webサーバを名乗るからには現在のWeb向けスクリプト言語No1としてのPHPをサポートしていないというのはいかにも心苦しい。PHPはApacheと関連付けて語られることも多い以上、Apacheの代替を目指しているわけではないDICEでサポートする意味も薄いと判断し敬遠してきたという経緯もあったものの、あまりにもPHPの勢いがありすぎるので仕方なくサポートに向けて舵を切ったというわけだ。特に海外では、VBulletinやWordPressといった代表的Webアプリケーションが利用できてようやくそれなりのWebサーバとしてユーザの検討の俎上に載せられることもあるだろう。<br />
<a name='more'></a><br />
PHPの魅力は、言語仕様ではなくバックエンドにある。特に、実行時間を限定できるオプションは有り難い。中をあまり見ていないので実装の詳細は知らないが、実行スレッド以外にスレッドを起動するか、あるいは元々のディスパッチャがディスパッチしたタスクを定期的にチェックするなりして監視していると思われる。WindowsだとTerminateThreadというAPIでスレッドを止めることが出来るけれども、これはリソースリークを起こす不安定なAPIとして知られている。特に、OSのスレッドプールを使っている場合は、スレッドを勝手に止めるとどんな予期しない悪影響が起こるかわかったものではない。PHP側で非ネイティブスレッドの実行を途中停止しているとすれば、PHPを組み込んで使うクライアントコード側としては手間が省けて大いに助かるというわけである。<br />
<br />
本記事の環境はVS2005(VC8)である。PHPはPHP 5.25を用いた。PHP組み込みに関する公式の資料は<a href="http://www.php.net/manual/en/internals2.php" target="_blank">PHPマニュアル第7章</a>に存在する。46節の"<a href="http://www.php.net/manual/en/internals2.ze1.zendapi.php" target="_blank">Zend API: Hacking the Core of PHP</a>"に変数の操作に関して記述がある。ところが、現時点のPHPマニュアルには、組み込みの際の初期化についてすら解説がない。PHPソースコードを展開した際に<code>php-5.2.5\sapi\embed</code>フォルダに見つかる<code>php_embed.h</code>ならびに<code>php_embed.c</code>を参照すると、初期化/終了マクロがあり、初期化の際に<code>sapi_startup</code>関数を呼んでいるのが分かる。これは<code>main\SAPI.h</code>ならびに<code>SAPI.c</code>に定義されており、引数となる構造体<code>_sapi_module_struct</code>にコールバックを登録してPHPのSAPI(Server API)を初期化している。そのコールバックの中でも、<code>php_embed_ub_write</code>がprintなどの出力を受ける関数で、<code>php_embed.c</code>内では<code>php_embed_ub_write</code>を経由して<code>php_embed_single_write</code>に至り、標準出力へ出力されている。つまりそこをオーバライドしてやればユーザ側でスクリプトの出力を受け取れるのであり、基本的にはRuby組み込みで使われる方法と同一だが、PHPはSAPIフレームワークの中でインターフェイスをユーザ側に提供している点が異なる。<br />
<br />
ユーザコードの概要が掴めたところで、次にPHP組み込みに必要な環境を作らなければならない。先に書いておくが、ここにまず問題がある。PHPのソースコードを<a href="http://www.php.net/downloads.php" target="_blank">ダウンロード</a>してローカルに展開した後に、マニュアルの<a href="http://jp.php.net/manual/ja/install.windows.building.php" target="_blank">Windows上でのビルド方法</a>に従ってビルドを行う。<code>win32build.zip</code>と <code>bindlib_w32.zip</code>もビルドに必要なGNUツールなどを含んでいるので所定の場所へ展開しておく必要がある。ビルドには、Visual C++ツールのコマンドプロンプトを使用する。今回は組み込みを行うので、WSHを経由して<code>configure.js</code>を呼び出しmakeの設定を行う際に、<code>--enable-embed</code>を引数に加えることになる。<br />
<br />
ところが、そのままビルドを始めると、エラーが出てストップしてしまう。エラーコードを見ると、libxmlに問題が起こっているらしい。そこで libxml2をダウンロードしてパスを通すも、やはりビルドが通らない。今度はどうやらiconvが必要なようで、最新版libiconvをダウンロー ドしてみると、なんと以前は入っていたVC++サポートがどういうわけか削除されていた。mingwでビルドせよとあるので試してみたがどうも上手くいかない。しかも、ダウンロード元がGNUだったことを思い出し、これはライセンス的にまずいのではないかと後から気付いた。iconvに由来するコードが PHPのスタティックライブラリに含まれているとすれば、PHP組み込みを行った時点でLGPLに汚染され、ユーザプログラムの配布に支障が出てしまう。 そこで、仕方なく<code>--without-libxml</code>をconfigure.jsのオプションに加え、XMLサポートを諦めた。調べてみると、iconv自体はPHP 5からPHP本体に組み込まれており、<code>php-5.2.5\ext\iconv</code>を見る限りクリーンルーム実装がなされている。このあたりlibxmlもどうにかできないものかと思うがここでは深く追及しない。<br />
<br />
libxml/iconvの問題以外にもビルドプロセスには問題があり、ZLIBの関数も使用できないので<code>--disable-zlib</code>で無効にする。これでようやく<code>php-5.2.5\Release_TS</code>ないし<code>php-5.2.5\Debug_TS</code>に、アプリケーションにリンクするスタティックライブラリ(<code>php5embed.lib</code>)と実行時に必要なdllファイル(<code>php5ts.dll</code>)が出来上がる。次にVC++の設定として、メニューの「ツール | オプション」からVC++のディレクトリにlibファイルの場所を追加し、さらにプロジェクト設定内のincludeファイルのフォルダに<code>php-5.2.5\</code>、<code>php-5.2.5\sapi\embed</code>、<code>php-5.2.5\Zend</code>、<code>php-5.2.5\TSRM</code>を加える。さらに、<code>PHP_WIN32</code>、<code>ZEND_WIN32</code>各シンボルの定義が必要となる。また、実際には、VC8からtime_tが64ビットになっているため<code>_USE_32BIT_TIME_T</code>も定義しなければクライアントコードのビルドは成功しない。以上でどうにか環境が整ったので、以下のコードがPHP組み込みのサンプルとしてビルド、実行できる。<br />
<br />
<pre class="brush: cpp"><br />
#include <stdio.h><br />
#include <tchar.h><br />
<br />
#include <iostream><br />
#include <string><br />
#include <cstdio><br />
<br />
#include <php.h><br />
#include <php_embed.h><br />
<br />
using namespace std;<br />
<br />
static int php_ub_write(const char* str, unsigned int str_length TSRMLS_DC)<br />
{<br />
string s(str, str_length);<br />
<br />
cout << s;<br />
<br />
return str_length;<br />
}<br />
<br />
static void php_log_message(char* message)<br />
{<br />
cerr << "php_log_message: " << message << endl;<br />
}<br />
<br />
static void php_sapi_error(int type, const char* fmt, ...)<br />
{<br />
va_list va;<br />
va_start(va, fmt);<br />
printf("php_sapi_error: ");<br />
vprintf(fmt, va);<br />
va_end(va);<br />
}<br />
<br />
int _tmain(int argc, _TCHAR* argv[])<br />
{<br />
php_embed_module.ub_write = php_ub_write;<br />
php_embed_module.log_message = php_log_message;<br />
php_embed_module.sapi_error = php_sapi_error;<br />
<br />
char* argv2[] = {""};<br />
<br />
PHP_EMBED_START_BLOCK(0, argv2);<br />
<br />
zval* v = NULL;<br />
<br />
MAKE_STD_ZVAL(v);<br />
ZVAL_STRING(v, "Hello", 1);<br />
ZEND_SET_SYMBOL(&amp;EG(symbol_table), "hello", v);<br />
<br />
zend_eval_string("print \"$hello \";$hello = \"World\";", NULL, "" TSRMLS_CC);<br />
<br />
if (v->type == IS_STRING)<br />
{<br />
cout << string(v->value.str.val, v->value.str.len) << endl;<br />
}<br />
<br />
PHP_EMBED_END_BLOCK();<br />
<br />
return 0;<br />
}<br />
<br />
</pre><br />
<br />
PHP関係のヘッダは他の標準ヘッダの後に置かなければエラーが出るようだ。コードの内容は、上述のように出力関数をオーバライドし、さらに<code>$hello</code>という名称の変数を作成して操作し最終的にHello Worldを表示するというたわいのないものである。<code>PHP_EMBED_START_BLOCK</code>、<code>PHP_EMBED_END_BLOCK</code>が初期化、終了のマクロで、内部で<code>zend_first_try</code>/<code>zend_catch</code>のブロックを作っているので変数のスコープに注意しなければならない。前回のPerl組み込みの場合だとPerlでの例外をラップする仕組みを自前で作る必要があったのに対し、PHPの方はZendが面倒を見てくれる点は有り難い。<br />
<br />
とはいえ、このようにしてビルドした物だとPHP 5の特徴的機能であるところのXMLがおそらく使用できない事はかなりの損失である。そこで、次に考えられる方法として、PHPの配布バイナリ内のライブラリを利用するという道がある。PHPの配布バイナリには、(一体どうやってビルドしたのかは謎だが)<code>php5embed.lib</code>と<code>php5ts.dll</code>が含まれているのである。Dependency Walkerで<code>php5ts.dll</code>を覗いてみてもCライブラリがスタティックリンクされているので一見使い勝手が良いように見える。ところが、落とし穴があり、デバッグ版だとVC++のdllが異なってくるので、これを使った場合正常にデバッグが出来なくなってしまう。そこで、開発の際には自分でビルドしたlibファイルを使い、リリース版は配布バイナリの方を利用することになる。<br />
<br />
しかし、拍子抜けではあるが、最終的にDICEでは上述のようなストレートな組み込み方法は採らなかった。というのも、不透明なライセンスの問題に加え、上述のやり方ではPHPスクリプトやASP(Active Server Pages)の最も有用な機能である、コードブロックのHTML埋め込みが出来ないのである(出来る方法があるのかも知れないがそれは本稿の範囲を超える)。これではこちらで配布するWebサーバに組み込んで利用するという本来の目的にそぐわない。そこで、結局DICEの方にISAPIエクステンションのサポートを入れ、ISAPIを経由してPHPをロードする形に落ち着いた。PHPはSAPIという組み込みインタフェイスを用意しており、PHPの配布バイナリにはISAPIエクステンションのdllが含まれている。この組み込み方法であればライセンス問題は完全にクリーンであり、PHP設定ファイルの読み込みなどの付随的処理もPHP側で全て自動的に行ってくれる。PHPはかなり素直なISAPIアクセスしか行わないので、別にwebサーバへの組み込み用途ではなくても、Windows上で ライセンスが気になる場合等はPHPそのものの組み込みの代わりにISAPIを経由してコントロールするのも一つの有力な選択肢ではないかと思う。他方で、配布を目的とせずサーバ側でのみ利用する場合はライセンスに関する問題はもちろん発生しないため、最初に取り上げた組み込み手順もそれはそれで有用となる局面もあるだろう。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-23416157201372797272008-01-15T16:58:00.000+09:002015-02-26T02:27:14.524+09:00Denial Ain't Just a River in Egypt久々にDICEの新バージョンをリリースすることが出来た。今回は互換性を破る変更もあったので、インクリメンタルに新バージョンをリリースするという以前の目標はひとまず措いて、必要な部品が全て入るまで待たざるを得なかった。ついでというわけではないが、自分のハンドルもKLからRyuKに変更した。KLでは短すぎるというのが主な理由だが、元のサイト名(KLassphere)をとある理由で変更したかったという動機が先に存在したかもしれない。そちらはryukwareへ変わっている。サイトのデザインもDICEのWeb UIで採用しているテンプレートを再利用して改装した。Webサイト自体も去年秋から新アドレスへ移転している。<br />
<a name='more'></a><br />
実はこのページもいよいよblogツールで生成するように変更しようかと思っていて、去年初めに各種ツールを検討してみた。が、一番最初に触ってみたMovable Typeがあまりにダメダメだったのでこの案はそこで一旦棚上げになってしまった。ファイルを置いているサーバはMySQLが使えるので環境は問題ない。ただ自分としてはデータのXCOPY deploymentができるSQLiteを使いたい。SSHも使えるのでSQLiteのビルドも出来、そこまでは問題なかった。問題だったのは、肝腎のMTの管理者ツールが非常に使いにくいということだ。カスタマイズの際の独自マークアップも汚く、そんな下らない物を学習する気など毛頭無い。使える既成 テーマもろくにない。何故MTが最初の候補に挙がったかというと、オリジナルサイトから古い記事をインポートしたときに、記事の日付をオリジナルの物に変更したいという事があり、さしあたりそれが可能なのがMTであったというわけだ。いつか機会が有れば今度はWordPressを試してみようと思ってはいるけれどSQLiteを用いるのにこちらはこちらで手間がかかるようで痛し痒しである。<br />
<br />
他人の作ったツールを使う場合の居心地の悪さというものはソフトウェアを書く際に利用するミドルウェアについてもしばしば感じることになる。オープンソースでソースコードとシンボルが手に入るならともかくそうでない場合は、いや手に入る場合であったとしても、分厚いミドルウェアの利用者はハッカーたることを強制されてしまう。その意味ではWindowsというものも巨大なミドルウェアであって、最近はMSDNのMicrosoft社員blogで様々な情報公開が行われていて助かることしきりである。しかし実態として見れば、不十分なAPIドキュメントあるいはAPIそのものについての補遺を非公式の場で行うことは現状を追認するエクスキューズでしかない。ミドルウェアによってもたらされる利益がある一方で、ミドルウェアを原因とするフラストレーションは相手がはっきりしているだけに顕在化しやすい。Windowsのような、選択の余地がないケースでは尚更である。<br />
<br />
DICEについて言うと、今回からSSPIを使うのをやめてOpenSSLに切り替えた。もう1つの変更は逆方向で、これまで8年間C++を用いる場合は常にSTLportを使い続けてきたのをやめて、今回からVC++のC++ライブラリを使うように変更した。VS2005からSxSになってインストールが面倒になったにも関わらず、である。C++標準ライブラリの変更自体は、これまでデバッグの関係で両対応にしていたので何ら問題はない。STLportの利点 は、かつてVC6時代にあったiostreamやロケールのバグが修正されていたり、SGI STL譲りのhash_map、ropeといったコンテナ、それからmalloc呼び出しを抑えるノードアロケータ、さらに名称の由来である移植性などで ある。加えて、後年メインテナーが変更になって以来、iostreamのさらなる修正やbig fileサポート、組み込みプラットフォームサポート、ユニットテスト追加、basic_stringのスタティックバッファ、+=ではなく+でbasic_stringを繋げる場合の最適化、ノードアロケータのロックフリー化、など様々な改良が施されてきた。VC++添付のライブラリはDinkumwareという会社のOEMで、そこのP.J. Plauger氏(以前Cマガジンで連載があった)が去年だったかニュースグループでSTLportの状況をかなり口汚く大人げないスタンスで論難し、反論に遭っていたのを見たことがある。<br />
<br />
STLportを捨てることを決めたのは、VS2008の機能リスト中に、STLとマネージド型のマーシャリングが自動的になされるといった.NET絡みの親和性を見つけて、これはもう避けられないと感じた事による。STLportを使っていると、他のC++ライブラリと併せて使った場合に問題が起こることが多く、その度に苦しめられたという問題に関しては、長年使ってきたという既成事実のおかげでどうにか切り抜けられてきていた。それでも、マルチコア環境でSTLportノー ドアロケータが実際は標準mallocに劣るというテスト結果(OSのヒープアロケータの薄いラッパーであるmallocの方がスレッドやスレッドローカルデータについてよく知っているのだから当たり前だ)や、VC++がC++例外のソースコードを提供していないせいでSTLportを使う場合もVC++のdllが丸ごと必要になるという問題は以前から不愉快に思っていた。ノードアロケータはユーザランドでのメモリリークになるが、引き替えにパフォーマンスが得られるならと思い許容していたにもかかわらず、実は期待した効果を得られていないというのは特にショックだった。また、DICEではそもそもiostreamを使っていないのでその部分でのSTLportの恩恵は全くない。<br />
<br />
STLportをやめてみてまず最初に判明したのは、これまでIntellisenseが正常に機能しなかった原因はSTLportにあったということだ。てっきりLokiが問題なのだと思い込んでいたが、そうではなかった。当然、MSVCのC++ライブラリだとSTLコンテナやbasic_stringの中身もデバッガで易々と見ることが出来る。唯一勿体ないと感じるのはbasic_stringの最適化くらいだけれども、どうしても無ければならないという類の物でもない。むしろ、デバッガでbasic_stringの中身を見るときに一々スタティックバッファとダイナミックバッファの両方を見たりせずに済むようになって、せいせいした。unordered associative container (hash_map)も今後TR1絡みで動いていくだろうから変更にはいい機会だったと思う。<br />
<br />
他方、VS2008には困ったこともあった。ATL属性が廃止になっていて、実はDICEのサービスはこれを使っている。しかし、実際に汚い物なので、廃止されるのもむべなるかなといったところである。今回のDICEは、こういったWindows世界での波風からできるだけ逃れるというのも一つのテーマになっていて、上記のMSVCのC++ライブラリへの回帰は残念ながらそれに逆行するが、このページの2006年11月30日の項で述べたWeb UIへの移行はついに達成した。Web UIでカバーしきれない部分は、これもMicrosoftの束縛の象徴であるMFCを使っていたDICEAdminShellを廃止し、DICE Managerという、機能を最低限に絞ったヘルパーアプリケーションをC#/Windows Formsで作って新たに入れた。2006年11月30日の項でも書いたようにMFCは本当に無茶苦茶で、DICEAdminShell廃止も MFCに直接起因している。VS2003からVS2005に移行したときにビルドし直したところ、実行ファイルがXPでは動くのにWindows Server 2003では動かないようになってしまったのだ。VS2008では驚くべき事にMFCがまだ残っていて機能拡張までされているそうだが、この辺で引導を渡してやっていた方が結果的に各方面が幸せだったのではないか。DICEの.NET WebアプリケーションコンテナもVS2005移行に伴って.NET Framework 1.1から2.0に移行し、マネージドコード部分に多少書き換えを要した。.NET Framework 3.5が2.0ベースであるのは賢明な方針である。<br />
<br />
Web UIは、それなりに道具が揃うとかなり楽に作れる。今回は勉強を兼ねて、prototype.jsに代表される外部のAJAXライブラリは一切使わず、自前でフレームワークを 作った。どのみちIDEが無いのだから大して手間は変わらない。無意味にファットなライブラリに興味はなく、また標準ライブラリを書き換えてあったり、一 旦導入すると以降はそのライブラリのスタイルを強要されたりするのも煩わしい。道具の一つとして、PHPのSmartyのようなテンプレートエンジンを JavaScriptでXHTMLのカスタムタグとDOMを利用して作ろうと思ったところ、XML名前空間やxhtmlについてIEはIE7でもサポートがろくになされていないのがわかり、またFirefoxにせよapplication/xml+xhtmlが必要になるため、仕方なくspanタグを使って作った。もう一つテンプレートエンジン絡みで参ったのは、<p>などのブロックタグが実行時にJavaScriptによって他のブロックタグの内側に挿入されると、IEが「未知の実行時エラー」を出すことだ。勿論規格的には正しい動作だが、普段はエラーに寛容なIEらしからぬ振る舞いで、VSのデバッガでもよくわからない部分へ行くので、原因を探すのに苦労した。<br />
<br />
今のところWeb UIは通常のwebページの外見で、Web IRCクライアントで利用したときにせいぜいタブインターフェイスをサポートするくらいだが、将来的にはWebデスクトップに必要なMDIをサポートできるように、ウィジェットクラスの階層を定義できたらと思っている。今回は、デモとして、管理者用にリモートファイルダウンロードマネージャを入れた。 GetRightやIrvineのようなダウンロードマネージャアプリケーションに似た機能をWebから利用できる。サポート機能はまだプリミティブで、ベーシック認証やリザルトコード302などはサポー トしていないが、HTTP/1.1のchunked encodingは完全にサポートしているので、<a href="https://www.blogger.com/perl_ruby_multithreading_embedding.html">Perl/Rubyで以前書いたツール</a>のスーパーセットになっている。<br />
<br />
Web UIについて書いていて今気付いたが、DICEに新しいライブラリをもう一つ導入している。メンバー登録時に表示されるCAPTCHAを、GDI+を使って実装した。今回Vistaを正式サポートした際に、諸々の理由でWindows 2000を動作環境から外したので、これが可能になった。とはいえGDIを使えば済む話でパフォーマンスもそちらの方が有利ではあるものの、GDI+は何かと楽である。オープンソースの画像ライブラリだと、最近はFirefoxなどで使われている、ハードウェアアクセラレーションが入ったCairoが一番有名だろうか。ただ、これも他のオープンソースライブラリの例に漏れずlibpngなどに依存しているので、環境を作るのに労力が要る。昔FreeBSD のサーバで使うC++の掲示板用にCAPTCHAを入れた時にはpaintlibというクロスプラットフォームライブラリを使っていて、これは2年前に開発中止になってしまっている。<br />
<br />
さて、今後は、DICEについてもう一段さらにジャンプを予定している。それが済んだらようやっと頻繁にリリースを出来る環境が出来るのではないかと思う。ゲームの方は、かなりの長期間離れていた後に、最近UT3が出たので下手ながらたまにオンライン対戦をやっている。UT2004は時間の無駄遣いが恐ろしくなりアンインストールするほどにプレイしたので今回も期待していたが、若干物足りない。売り上げは残念な結果に終わっているのでいよいよPCゲームも(MMOとSteamで買えるゲーム以外は)終焉に至ったかと感慨深い。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-80313485640678787872007-12-22T16:02:00.000+09:002015-02-26T02:29:39.267+09:00SSL Server with OpenSSL Memory BIO a.k.a. Prerequisite to AsynchronousOpenSSLIn the <a href="http://zzz.zggg.com/2005/05/05/how-to-programmatically-create-self-signed-cert-key-pair-windows-sspi/" target="_blank">last article of mine about SSL-related programming</a>, the API to handle SSL transaction for the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> was the <a href="http://msdn2.microsoft.com/en-us/library/aa380493.aspx" target="_blank">SSPI</a> (Security Support Provider Interface) that is one of the standard API sets provided by Microsoft Windows. Though I outlined why I chose SSPI over <a href="http://www.openssl.org/" target="_blank">OpenSSL</a> in the article, recently I replaced SSPI with OpenSSL in the latest version of the DICE that was released with HTTPS implemented. The rationale behind the switch of the SSL engine was not so straightforward.<br />
<br />
For me, the main concern about OpenSSL had been its putative close relationship with the BSD socket architecture that is not compatible with asynchronous sockets and I/O completion ports. Another concern was about OpenSSL's vulnerabilities against security breaches. OpenSSL has been an active target by crackers and one of the most scrutinized library. Not that Microsoft's implementation is any better, but as far as I know OpenSSL gets many security advisories about it through its update history.<br />
<a name='more'></a><br />
<br />
On the other hand, SSPI's complex nature as an abstraction layer for multiple protocols had been a growing pain. Since the DICE is built with I/O completion ports, it was pretty messy to embed SSPI negotiation in combination with asynchronous sockets. After the sour experience of implementing IRC over SSL with SSPI on asynchrounous sockets and I/O completion ports some years ago, I was not so thrilled to touch it again to implement HTTPS. I needed a better, simpler framework to get the job done.<br />
<br />
In updating the DICE to 0.9.0.0 after a long hiatus, I took the new version as a chance to address awkward points that had been long overlooked, sometimes even breaking compatibility with older versions. One of such problems in the DICE was SSL-related implementation. That's why I took a second look at OpenSSL. Though it may contain unknown security flaws, its core cryptographic functions are actively upgraded to the latest standard spec unlike Microsoft's implementation. Also it's nice in terms of compiler/linker global optimization that the source code is available.<br />
<br />
So what is the key to the actual usage of OpenSSL in asynchronous sockets? I browsed the sparse user manual and found the necessary tool for me: <a href="http://www.openssl.org/docs/crypto/BIO_s_mem.html" target="_blank">memory BIO</a>.<br />
<br />
<a href="http://www.openssl.org/docs/crypto/bio.html" target="_blank">BIO</a> is an "I/O abstraction, it hides many of the underlying I/O details from an application" according to the OpenSSL document (What does B in BIO stand for anyway?). Also a "memory BIO is a source/sink BIO which uses memory for its I/O." This basically means you can do SSL negotiation without touching a BSD socket or other platform-specific stuff at all. All you need to do is feed encrypted data into a memory BIO until it's ready to dump something meaningful decrypted out of it.<br />
<br />
While the title of this article mentions "asynchronous", the sample code below uses traditional synchronous sockets. In fact, the simple abstraction by memory BIO allows smooth transition from synchronous sockets to asynchronous sockets since buffering memory BIO is exactly the same process as parsing a plain message by waiting for a whole message packet to arrive in asynchronous callbacks. In addition, you had to manually set up an SSL handshake at the beginning of a connection by yourself in SSPI while BIO in OpenSSL completely hides it from a client code. I could replace SSPI with OpenSSL for the already implemented IRC over SSL, and then could implement HTTPS in the latest version of the DICE by making the flow described in this sample code asynchronous.<br />
<br />
The only caveat in making it asynchronous is it's a bit difficult to support SSL renegotiation. Though it's rare, sometimes SSL resets SSL properties at the middle of connection to make it difficult for crackers to collect necessary data. When it happens, you have to save the context such as a partially decrypted message, then do handshake again, and go back to where you originally were by restoring the original state in a new SSL context. In synchronous connection all these steps can occur between 2 braces in a certain function, but in asynchronous sockets it's hard to suspend it by saving the context and resume it from the original point, since you have to juggle many more states in your session state machine to fully support renegotiation.<br />
<br />
This test code of a synchronous HTTPS server was written and tested with VS2005 SP1 on Windows XP SP2. You have to build OpenSSL by obtaining its source code prior to building this sample. The OpenSSL version I used was 0.9.8g. After building OpenSSL, you have to add <code>libeay32.lib</code> and <code>ssleay32.lib</code> in the <code>out32</code> folder under the OpenSSL source root directory to the linker input option in the VC++ project of the sample code, in addition to <code>ws2_32.lib</code> for Winsock 2.<br />
<br />
<pre class="brush: cpp">// openssltest.cpp<br />
<br />
// InitializeCriticalSectionAndSpinCount<br />
#ifndef _WIN32_WINNT<br />
#define _WIN32_WINNT 0x0500<br />
#endif _WIN32_WINNT<br />
<br />
#include <windows.h><br />
<br />
#include <tchar.h><br />
<br />
#include <openssl/ssl.h><br />
#include <openssl/err.h><br />
<br />
#include <iostream><br />
#include <string><br />
<br />
using namespace std;<br />
<br />
</pre><br />
<br />
To use OpenSSL in a multithread application you have to implement necessary <a href="http://www.openssl.org/docs/crypto/threads.html" target="_blank">synchronization primitives for OpenSSL</a> with your platform-supplied functions. In this sample application, I used CRITICAL_SECTION and <a href="http://msdn2.microsoft.com/en-us/library/ms683476.aspx" target="_blank"><code>InitializeCriticalSectionAndSpinCount</code></a> available in Windows 2000 and later. Its wrapper class <code>Synchronizer</code> does synchronization in various OpenSSL callbacks.<br />
<br />
<pre class="brush: cpp">class Synchronizer<br />
{<br />
private:<br />
CRITICAL_SECTION lock_;<br />
<br />
public:<br />
__forceinline Synchronizer()<br />
{<br />
InitializeCriticalSectionAndSpinCount(&amp;lock_, 4000);<br />
}<br />
__forceinline ~Synchronizer()<br />
{<br />
DeleteCriticalSection(&amp;lock_);<br />
}<br />
<br />
__forceinline void acquire()<br />
{<br />
__try<br />
{<br />
EnterCriticalSection(&amp;lock_);<br />
}<br />
__except (EXCEPTION_EXECUTE_HANDLER)<br />
{<br />
}<br />
}<br />
<br />
__forceinline void release()<br />
{<br />
LeaveCriticalSection(&amp;lock_);<br />
}<br />
};<br />
<br />
static Synchronizer* openssl_locks = 0;<br />
<br />
struct CRYPTO_dynlock_value<br />
{<br />
Synchronizer s;<br />
};<br />
<br />
static void funcOpenSSLLockingCallback(<br />
int mode,<br />
int type,<br />
const char* file,<br />
int line<br />
)<br />
{<br />
if (mode &amp; CRYPTO_LOCK)<br />
openssl_locks[type].acquire();<br />
else<br />
openssl_locks[type].release();<br />
}<br />
<br />
static unsigned long funcOpenSSLIDCallback(void)<br />
{<br />
return GetCurrentThreadId();<br />
}<br />
<br />
static CRYPTO_dynlock_value* funcOpenSSLDynCreateCallback(<br />
const char* file,<br />
int line<br />
)<br />
{<br />
return new CRYPTO_dynlock_value;<br />
}<br />
<br />
static void funcOpenSSLDynDestroyCallback(<br />
CRYPTO_dynlock_value* l,<br />
const char* file,<br />
int line<br />
)<br />
{<br />
delete l;<br />
}<br />
<br />
static void funcOpenSSLDynLockCallback(<br />
int mode,<br />
CRYPTO_dynlock_value* l,<br />
const char* file,<br />
int line<br />
)<br />
{<br />
if (mode &amp; CRYPTO_LOCK)<br />
l->s.acquire();<br />
else<br />
l->s.release();<br />
}<br />
<br />
</pre><br />
<br />
A function to get an <code>EVP_PKEY</code> RSA private key object.<br />
<br />
<pre class="brush: cpp">EVP_PKEY* get_evp_pkey(RSA* rsa, int priv)<br />
{<br />
RSA* key = (priv ? RSAPrivateKey_dup(rsa): RSAPublicKey_dup(rsa));<br />
if (!key)<br />
goto error;<br />
<br />
EVP_PKEY* pkey = EVP_PKEY_new();<br />
if (!pkey)<br />
goto error;<br />
<br />
if (!(EVP_PKEY_assign_RSA(pkey, key)))<br />
goto error;<br />
<br />
return pkey;<br />
<br />
error:<br />
if (pkey)<br />
EVP_PKEY_free(pkey);<br />
<br />
if (key)<br />
RSA_free(key);<br />
<br />
return NULL;<br />
}<br />
<br />
</pre><br />
<br />
A function to generate an X509-format certificate and RSA public key. It's signed by a private key.<br />
<br />
<pre class="brush: cpp">static X509* create_certificate(<br />
RSA* rsa,<br />
RSA* rsaSign,<br />
const char* cname,<br />
const char* cnameSign,<br />
const char* pszOrgName,<br />
unsigned int certLifetime<br />
)<br />
{<br />
time_t start_time = time(NULL);<br />
time_t end_time = start_time + certLifetime;<br />
<br />
EVP_PKEY* sign_pkey = get_evp_pkey(rsaSign, 1);<br />
if (!sign_pkey)<br />
goto error;<br />
<br />
EVP_PKEY* pkey = get_evp_pkey(rsa, 0);<br />
if (!pkey)<br />
goto error;<br />
<br />
X509* x509 = X509_new();<br />
if (!x509)<br />
goto error;<br />
if (!(X509_set_version(x509, 2)))<br />
goto error;<br />
if (!(ASN1_INTEGER_set(X509_get_serialNumber(x509), (long)start_time)))<br />
goto error;<br />
<br />
X509_NAME* name = X509_NAME_new();<br />
if (!name)<br />
goto error;<br />
<br />
int nid = OBJ_txt2nid("organizationName");<br />
if (nid == NID_undef)<br />
goto error;<br />
if (!(X509_NAME_add_entry_by_NID(name, nid, MBSTRING_ASC, (unsigned char*)pszOrgName, -1, -1, 0)))<br />
goto error;<br />
if ((nid = OBJ_txt2nid("commonName")) == NID_undef)<br />
goto error;<br />
if (!(X509_NAME_add_entry_by_NID(name, nid, MBSTRING_ASC, (unsigned char*)cname, -1, -1, 0)))<br />
goto error;<br />
if (!(X509_set_subject_name(x509, name)))<br />
goto error;<br />
<br />
X509_NAME* name_issuer = X509_NAME_new();<br />
if (!name_issuer)<br />
goto error;<br />
if ((nid = OBJ_txt2nid("organizationName")) == NID_undef)<br />
goto error;<br />
if (!(X509_NAME_add_entry_by_NID(name_issuer, nid, MBSTRING_ASC, (unsigned char*)pszOrgName, -1, -1, 0)))<br />
goto error;<br />
if ((nid = OBJ_txt2nid("commonName")) == NID_undef)<br />
goto error;<br />
if (!(X509_NAME_add_entry_by_NID(name_issuer, nid, MBSTRING_ASC, (unsigned char*)cnameSign, -1, -1, 0)))<br />
goto error;<br />
if (!(X509_set_issuer_name(x509, name_issuer)))<br />
goto error;<br />
<br />
if (!X509_time_adj(X509_get_notBefore(x509), 0, &amp;start_time))<br />
goto error;<br />
<br />
if (!X509_time_adj(X509_get_notAfter(x509), 0, &amp;end_time))<br />
goto error;<br />
if (!X509_set_pubkey(x509, pkey))<br />
goto error;<br />
if (!X509_sign(x509, sign_pkey, EVP_sha1()))<br />
goto error;<br />
<br />
goto done;<br />
<br />
error:<br />
if (x509)<br />
{<br />
X509_free(x509);<br />
x509 = NULL;<br />
}<br />
<br />
done:<br />
if (sign_pkey)<br />
EVP_PKEY_free(sign_pkey);<br />
if (pkey)<br />
EVP_PKEY_free(pkey);<br />
if (name)<br />
X509_NAME_free(name);<br />
if (name_issuer)<br />
X509_NAME_free(name_issuer);<br />
<br />
return x509;<br />
}<br />
<br />
</pre><br />
<br />
An error report function for an SSL object.<br />
<br />
<pre class="brush: cpp">void reportError(SSL* ssl, int result)<br />
{<br />
if (result <= 0)<br />
{<br />
int error = SSL_get_error(ssl, result);<br />
<br />
switch (error)<br />
{<br />
case SSL_ERROR_ZERO_RETURN:<br />
cout << "SSL_ERROR_ZERO_RETURN" << endl;<br />
break;<br />
case SSL_ERROR_NONE:<br />
cout << "SSL_ERROR_NONE" << endl;<br />
break;<br />
case SSL_ERROR_WANT_READ:<br />
cout << "SSL_ERROR_WANT_READ" << endl;<br />
break;<br />
default:<br />
{<br />
char buffer[256];<br />
<br />
while (error != 0)<br />
{<br />
ERR_error_string_n(error, buffer, sizeof(buffer));<br />
<br />
cout << "Error: " << error << " - " << buffer << endl;<br />
<br />
error = ERR_get_error();<br />
}<br />
}<br />
<br />
break;<br />
}<br />
}<br />
}<br />
</pre><br />
<br />
The main function begins by setting up OpenSSL synchronization primitives, then creates a new SSL context object <code>SSL_CTX</code>. This global OpenSSL context should not be mistaken as the per-connection context object <code>SSL</code> that will be introduced later.<br />
<br />
<pre class="brush: cpp">int _tmain(int argc, _TCHAR* argv[])<br />
{<br />
#ifdef _DEBUG<br />
// OpenSSL internal memory-leak checkers<br />
CRYPTO_malloc_debug_init();<br />
CRYPTO_dbg_set_options(V_CRYPTO_MDEBUG_ALL);<br />
CRYPTO_mem_ctrl(CRYPTO_MEM_CHECK_ON);<br />
#endif<br />
<br />
openssl_locks = new Synchronizer[CRYPTO_num_locks()];<br />
<br />
// callbacks for static lock<br />
CRYPTO_set_locking_callback(funcOpenSSLLockingCallback);<br />
CRYPTO_set_id_callback(funcOpenSSLIDCallback);<br />
<br />
// callbacks for dynamic lock<br />
CRYPTO_set_dynlock_create_callback(funcOpenSSLDynCreateCallback);<br />
CRYPTO_set_dynlock_destroy_callback(funcOpenSSLDynDestroyCallback);<br />
CRYPTO_set_dynlock_lock_callback(funcOpenSSLDynLockCallback);<br />
<br />
// Load algorithms and error strings.<br />
SSL_load_error_strings();<br />
SSL_library_init();<br />
<br />
// Compatible with SSLv2, SSLv3 and TLSv1<br />
SSL_METHOD* method = SSLv23_server_method();<br />
<br />
// Create new context from method.<br />
SSL_CTX* ctx = SSL_CTX_new(method);<br />
<br />
// SSL_new() creates a new SSL structure which is needed to hold the data for a TLS/SSL connection.<br />
// The new structure inherits the settings of the underlying context ctx: connection method (SSLv2/v3/TLSv1),<br />
// options, verification settings, timeout settings.<br />
<br />
</pre><br />
<br />
The next thing to do is key-pair/certificate generation for public key en/decryption. This sample application uses a self-signed certificate to omit the need of a valid certificate.<br />
<br />
<pre class="brush: cpp"> RSA* rsa = RSA_generate_key(1024, 65537, NULL, NULL);<br />
if (!rsa)<br />
{<br />
cerr << "RSA_generate_key" << endl;<br />
return 1;<br />
}<br />
<br />
const char* pszCommonName = "test name";<br />
X509* cert = create_certificate(rsa, rsa, pszCommonName, pszCommonName, "DICE", 3 * 365 * 24 * 60 * 60 /* 3 years */);<br />
if (!cert)<br />
{<br />
cerr << "Couldn't create a certificate" << endl;<br />
return 1;<br />
}<br />
<br />
///////////////////////////////////////////////////////////////////////////<br />
<br />
if (SSL_CTX_use_RSAPrivateKey(ctx, rsa) <= 0)<br />
{<br />
ERR_print_errors_fp(stderr);<br />
return 1;<br />
}<br />
<br />
if (SSL_CTX_use_certificate(ctx, cert) <= 0)<br />
{<br />
ERR_print_errors_fp(stderr);<br />
return 1;<br />
}<br />
<br />
RSA_free(rsa);<br />
<br />
X509_free(cert);<br />
<br />
if (!SSL_CTX_check_private_key(ctx))<br />
{<br />
cerr << "Private key is invalid." << endl;<br />
return 1;<br />
}<br />
else<br />
cout << "Private key is OK" << endl;<br />
<br />
//////////////////////////////////////////////////////////////////////////////////<br />
<br />
</pre><br />
<br />
Setting up Winsock 2 and a listener socket to create a network server.<br />
<br />
<pre class="brush: cpp"> cout << "Preparing socket" << endl;<br />
<br />
WORD wVersionRequested = MAKEWORD(2,1);<br />
WSADATA wsaData;<br />
int nRet = WSAStartup(wVersionRequested, &amp;wsaData);<br />
<br />
if (wsaData.wVersion != wVersionRequested)<br />
{<br />
cerr << "Wrong version" << endl;<br />
return 1;<br />
}<br />
<br />
SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);<br />
if (listenSocket == INVALID_SOCKET)<br />
{<br />
cerr << "socket()" << endl;<br />
return 1;<br />
}<br />
<br />
SOCKADDR_IN saServer;<br />
saServer.sin_family = AF_INET;<br />
saServer.sin_addr.s_addr = INADDR_ANY;<br />
saServer.sin_port = htons(443);<br />
<br />
nRet = bind(listenSocket, (LPSOCKADDR)&amp;saServer, sizeof(struct sockaddr));<br />
if (nRet == SOCKET_ERROR)<br />
{<br />
closesocket(listenSocket);<br />
cerr << "bind()" << endl;<br />
return 1;<br />
}<br />
<br />
nRet = listen(listenSocket, SOMAXCONN);<br />
if (nRet == SOCKET_ERROR)<br />
{<br />
closesocket(listenSocket);<br />
cerr << "listen()" << endl;<br />
return 1;<br />
}<br />
<br />
bool bQuit = false;<br />
<br />
cout << "Ready" << endl;<br />
<br />
</pre><br />
<br />
Everything is ready, now let's get into the actual server connection loop. When it accepts a new connection, it creates a new SSL object that encapsulates a single SSL connection. Also it creates memory BIO buffers for input and output.<br />
<br />
<pre class="brush: cpp"> while (1)<br />
{<br />
SSL* ssl = SSL_new(ctx);<br />
BIO* bioIn = BIO_new(BIO_s_mem());<br />
BIO* bioOut = BIO_new(BIO_s_mem());<br />
<br />
// SSL_set_bio() connects the BIOs rbio and wbio for the read and write operations of the TLS/SSL<br />
// (encrypted) side of ssl.<br />
SSL_set_bio(ssl, bioIn, bioOut);<br />
<br />
SSL_set_accept_state(ssl);<br />
<br />
SOCKET remoteSocket = accept(<br />
listenSocket,<br />
NULL,<br />
NULL<br />
);<br />
<br />
if (remoteSocket == INVALID_SOCKET)<br />
{<br />
closesocket(listenSocket);<br />
cerr << "accept()" << endl;<br />
return 1;<br />
}<br />
<br />
cout << "Accepted" << endl;<br />
<br />
char szBuf[128];<br />
<br />
memset(szBuf, 0, sizeof(szBuf));<br />
<br />
bool bHandShakeOver = false;<br />
bool bReplyOver = false;<br />
<br />
const char* reply = "HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 14\r\nConnection: close\r\n\r\nthis is a test";<br />
<br />
string strBuffer;<br />
<br />
while (1)<br />
{<br />
nRet = recv(remoteSocket, szBuf, sizeof(szBuf), 0);<br />
if (nRet == SOCKET_ERROR)<br />
{<br />
cerr << "recv() - SOCKET_ERROR" << endl;<br />
break;<br />
}<br />
<br />
if (nRet > 0)<br />
{<br />
int bufferUsed = BIO_write(bioIn, szBuf, nRet);<br />
if (bufferUsed == -1 || bufferUsed == -2 || bufferUsed == 0)<br />
{<br />
// error<br />
}<br />
<br />
// SSL read loop<br />
while (1)<br />
{<br />
// SSL_read() tries to read num bytes from the specified ssl into the buffer buf.<br />
int bytesOut = SSL_read(ssl, (void*)szBuf, sizeof(szBuf));<br />
if (bytesOut > 0)<br />
{<br />
strBuffer.append(szBuf, bytesOut);<br />
<br />
if ((strBuffer.size() > 4 &amp;&amp; strBuffer.find("\r\n\r\n", strBuffer.size() - 4) != string::npos)<br />
|| (strBuffer.size() > 2 &amp;&amp; strBuffer.find("\n\n", strBuffer.size() - 2) != string::npos)<br />
)<br />
{<br />
cout << "message size: " << strBuffer.size() << endl << "message: " << strBuffer << endl;<br />
<br />
bReplyOver = true;<br />
<br />
if (strBuffer.find("quit") != string::npos)<br />
bQuit = true;<br />
<br />
// SSL_write() writes num bytes from the buffer buf into the specified ssl connection.<br />
int result = SSL_write(ssl, reply, strlen(reply));<br />
if (result <= 0)<br />
{<br />
reportError(ssl, result);<br />
bReplyOver = true;<br />
}<br />
<br />
break;<br />
}<br />
}<br />
else<br />
{<br />
if (SSL_want_read(ssl))<br />
{<br />
cout << "SSL_want_read" << endl;<br />
}<br />
else<br />
{<br />
reportError(ssl, bytesOut);<br />
bReplyOver = true;<br />
break;<br />
}<br />
<br />
if (!bHandShakeOver &amp;&amp; SSL_is_init_finished(ssl))<br />
{<br />
cout << "Handshake has been finished" << endl;<br />
bHandShakeOver = true;<br />
<br />
char cipdesc[128];<br />
SSL_CIPHER* sc = SSL_get_current_cipher(ssl);<br />
if (sc)<br />
cout << "encryption: " << SSL_CIPHER_description(sc, cipdesc, sizeof(cipdesc)) << endl;<br />
}<br />
<br />
break;<br />
}<br />
}<br />
}<br />
else if (nRet == 0)<br />
{ // the connection has been gracefully closed<br />
break;<br />
}<br />
<br />
while (1)<br />
{<br />
// BIO_ctrl_pending() returns the number of bytes buffered in a BIO.<br />
size_t pending = BIO_ctrl_pending(bioOut);<br />
if (pending > 0)<br />
{<br />
cout << "BIO_ctrl_pending(bioOut) == " << pending << endl;<br />
<br />
// BIO_read() attempts to read len bytes from BIO b and places the data in buf.<br />
int bytesToSend = BIO_read(bioOut, (void*)szBuf, sizeof(szBuf) > pending ? pending : sizeof(szBuf));<br />
if (bytesToSend > 0)<br />
{<br />
cout << "BIO_read(bioOut) == " << bytesToSend << endl;<br />
<br />
int sent = 0;<br />
while (1)<br />
{<br />
nRet = send(remoteSocket, szBuf + sent, bytesToSend, 0);<br />
if (nRet == SOCKET_ERROR)<br />
{<br />
bReplyOver = true;<br />
cerr << "send() - SOCKET_ERROR" << endl;<br />
break;<br />
}<br />
else<br />
{<br />
sent += nRet;<br />
bytesToSend -= nRet;<br />
if (bytesToSend == 0)<br />
break;<br />
}<br />
}<br />
}<br />
else if (!BIO_should_retry(bioOut))<br />
{// BIO_should_retry() is true if the call that produced this condition should then be retried at a later time.<br />
reportError(ssl, bytesToSend);<br />
}<br />
}<br />
else<br />
{<br />
cout << "BIO_ctrl_pending(bioOut) == 0" << endl;<br />
break;<br />
}<br />
}<br />
<br />
if (bReplyOver)<br />
{<br />
cout << "post-reply" << endl;<br />
break;<br />
}<br />
}<br />
<br />
</pre><br />
<br />
After it's done you have to clean up all resources. If there's a memory leak, CRYPTO_mem_leaks_fp will print it out.<br />
<br />
<pre class="brush: cpp"> closesocket(remoteSocket);<br />
<br />
// this frees associated BIO too, so no need for BIO_free<br />
SSL_free(ssl);<br />
<br />
if (bQuit)<br />
break;<br />
}<br />
<br />
closesocket(listenSocket);<br />
<br />
WSACleanup();<br />
<br />
//////////////////////////////////////////////////////////////////////////////////<br />
<br />
SSL_CTX_free(ctx);<br />
<br />
CRYPTO_set_dynlock_create_callback(NULL);<br />
CRYPTO_set_dynlock_destroy_callback(NULL);<br />
CRYPTO_set_dynlock_lock_callback(NULL);<br />
<br />
CRYPTO_set_locking_callback(NULL);<br />
CRYPTO_set_id_callback(NULL);<br />
<br />
delete[] openssl_locks;<br />
openssl_locks = 0;<br />
<br />
EVP_cleanup();<br />
CRYPTO_cleanup_all_ex_data();<br />
ERR_remove_state(0);<br />
ERR_free_strings();<br />
<br />
#ifdef _DEBUG<br />
CRYPTO_mem_leaks_fp(stdout);<br />
#endif<br />
<br />
return 0;<br />
}<br />
<br />
</pre><br />
<br />
When you run this sample code it listens to the standard HTTPS port 443. Start with a web browser and open "https://127.0.0.1/" to see how an SSL connection is processed. To stop this server, put "quit" in a request URL ("https://127.0.0.1/quit").久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-12836068242299442592007-12-12T12:07:00.000+09:002015-02-26T02:30:47.989+09:00C++ Asynchronous Delegate for Microsoft WindowsMicrosoft Windows 2000 and later have a very useful system function to make an asynchronous function call: <a href="http://msdn2.microsoft.com/en-us/library/ms684957.aspx" target="_blank"><code>QueueUserWorkItem</code></a>. With this function and its thread pool that is aware of what Windows is actually doing at a given time, Windows takes care of all asynchronous function call complicatedness for you in the simplest form. This high-level function is a god-send for lazy programmers who would concentrate on what an application can do in a reasonable performance range rather than bothering about how it does things with the smallest performance hit.<br/><br/>But people can never be lazy enough, setting it up with context information each time will soon become a boring task especially when you want to asynchronously call a member function of a C++ object. But it's not possible to make it completelly dynamic, either. You have to manually write a wrapper function, since <a href="http://msdn2.microsoft.com/en-us/library/ms684957.aspx" target="_blank"><code>QueueUserWorkItem</code></a> is a mere C function that knows jack about C++. This article introduces a minimalistic toolkit <code>AsyncDelegate.h</code> that lends itself to solving this issue by using C++ templates.<br/><a name='more'></a><br/>To use <code>AsyncDelegate.h</code>, declare an asynchronously called member function and define an inner class for context information. This inner class can have a ctor/dtor if needed.<br/><br/><pre class="brush: cpp">#include <string><br/>#include "AsyncDelegate.h"<br/><br/>class X<br/>{<br/>public:<br/><br/>// ...<br/><br/> struct ContextY : AsyncDelegateContextHeader<X><br/> { // you can add parameters as members.<br/> std::string strParameter_;<br/> };<br/><br/> void callbackY(ContextY* pContext);<br/>};<br/><br/></pre><br/><br/><code>X::callbackY</code> is defined like this in a .cpp file:<br/><br/><pre class="brush: cpp">void X::callbackY(ContextY* pContext)<br/>{<br/> // do something with pContext_->strParameter_<br/>}<br/><br/></pre><br/><br/>Now you can activate it anywhere you want where a pointer to a target X object is available.<br/><br/><pre class="brush: cpp">X* p = new X;<br/><br/>// ...<br/><br/>{<br/> AsyncDelegate<<br/> X,<br/> X::ContextY,<br/> &amp;amp;X::callbackY<br/> > c(p);<br/><br/> c.pContext_->strParameter_ = &quot;parameter&quot;;<br/><br/> // an asynchronous call to X::callbackY is implicitly initiated here by the destructor of c<br/>}<br/><br/></pre><br/><br/><code>X::callbackY</code> is called in the context of the specific <code>X</code> object <code>p</code> with a <code>ContextY</code> object as a parameter. In other words, <code>AsyncDelegate.h</code> is a tool to do deferred execution of this member function call.<br/><br/><pre class="brush: cpp">X* p = new X;<br/><br/>ContextY pContext;<br/><br/>p->callbackY(pContext);<br/><br/></pre><br/><br/>Let's take a look at the actual <code>AsyncDelegate.h</code>.<br/><br/><pre class="brush: cpp">// AsyncDelegate.h<br/><br/>#ifndef CALC_ASYNCDELEGATE_H<br/>#define CALC_ASYNCDELEGATE_H<br/><br/>#ifndef _WIN32_WINNT<br/>#define _WIN32_WINNT 0x0500<br/>#endif _WIN32_WINNT<br/><br/>#include <windows.h><br/><br/></pre><br/><br/><code>AsyncDelegateContextHeader</code> is inserted before a context object as a hidden information header that knows to which object this call belongs.<br/><br/><pre class="brush: cpp"><br/>template <class T><br/>struct AsyncDelegateContextHeader<br/>{<br/> T* pSelf_;<br/>};<br/></pre><br/><br/>This class casts a void parameter into T and destructs it at the end of a function call.<br/><br/><pre class="brush: cpp"><br/>template <class T><br/>class AsyncDelegateContextCleaner<br/>{<br/>private:<br/> AsyncDelegateContextCleaner() {}<br/>public:<br/> T* pContext_;<br/><br/> explicit AsyncDelegateContextCleaner(LPVOID lpParameter) : pContext_((T*)lpParameter) {}<br/> ~AsyncDelegateContextCleaner()<br/> {<br/> delete(pContext_);<br/> }<br/>};<br/></pre><br/><br/><code>MemFunPointerCreator</code> is a class to make a C++ member function pointer type from 2 template parameter types. This class is required to put a member function pointer in the template parameters of <code>queueUserWorkItemThreadProc</code>.<br/><br/><pre class="brush: cpp">template <class T, class C><br/>struct MemFunPointerCreator<br/>{<br/> typedef void (T::* ClientMemFunPointerType)(C*);<br/>};<br/></pre><br/><br/>This function template creates a function called by <code>QueueUserWorkItem</code>. A C++ member function pointer created by <code>MemFunPointerCreator</code> is set as a non-type template parameter.<br/><br/><pre class="brush: cpp">template <class T, class C, typename MemFunPointerCreator<T, C>::ClientMemFunPointerType pMemberFunction><br/>DWORD WINAPI queueUserWorkItemThreadProc(LPVOID lpParameter)<br/>{<br/> AsyncDelegateContextCleaner<C> c(lpParameter);<br/><br/> ((c.pContext_->pSelf_)->*pMemberFunction)(c.pContext_);<br/><br/> return 1;<br/>}<br/><br/></pre><br/><br/><span style="font-family: Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif; line-height: 19px; white-space: normal; font-size: 13px;">This is the main class exposed to a user. If <code>bLong</code> is true <code>QueueUserWorkItem</code> is called with <code>WT_EXECUTELONGFUNCTION</code> that implies spawning a new thread which is the default behavior of this tool.</span><br/><br/><pre class="brush: cpp">template <<br/> class T,<br/> class C,<br/> typename MemFunPointerCreator<T, C>::ClientMemFunPointerType pMemberFunction,<br/> bool bLong_ = true<br/>><br/>struct AsyncDelegate<br/>{<br/> C* pContext_;<br/><br/> explicit AsyncDelegate(T* p)<br/> {<br/> pContext_ = new C;<br/> pContext_->pSelf_ = p;<br/> }<br/><br/> ~AsyncDelegate()<br/> {<br/> QueueUserWorkItem(<br/> queueUserWorkItemThreadProc<T, C, pMemberFunction>, // starting address<br/> pContext_, // function data<br/> bLong_ ? WT_EXECUTELONGFUNCTION : WT_EXECUTEDEFAULT // worker options<br/> );<br/> }<br/>};<br/><br/>#endif CALC_ASYNCDELEGATE_H<br/><br/></pre><br/><br/><span style="font-family: Georgia, 'Times New Roman', 'Bitstream Charter', Times, serif; line-height: 19px; white-space: normal; font-size: 13px;">It may be counter-intuitive to call this tool a "delegate" as it doesn't support copy and other smart pointer properties, but it's OK for me because its usage is limited to kicking a worker thread immediately.</span>久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-16478691902442582202007-01-11T17:22:00.000+09:002015-02-26T02:33:43.818+09:00はてなWebサービスAPIを用いたPerl/Ruby Webアプリケーション2題Webサービスというものが新しい技術として流行ったのは2001年頃だった。そこで語られていたビジョンというのは、WSDLで定義したWebサービスAPIをUDDIに登録し、それらがSOAPで通信しながらWeb上に広がるアプリケーションを構成するというようなものである。MS Windows的に言うと、レジストリに登録されたCOMコンポーネントのインターフェイス発見/呼び出しメカニズムのインターネット版ということにな る。ただし、Windowsの場合は、DCOMやCOM+といった、Windowsシステム同士のネットワークやWindowsシステム内部を一貫した分散オブジェクトRPCの文脈でとらえる仕組みを経て、一度レガシーを整理し、.NETに至る。このシステムをビルディングブロックとして利用することが宣伝されたHailstormというマイクロソフト提供のプラットフォームは、個人向けWebサービスをUDDI経由で提供するという触れ込みだった。その後Hailstormは、シングルサインオン認証をめぐる覇権争いに巻き込まれた挙げ句、開放された世界での商業的キーワードとしては消滅してしまった。他方、同時期にローンチして以来、コントロールされた閉鎖環境で運営されてきているシングルサインオンの理想型が、有料サービスXbox Liveとして存続している。<br/><a name='more'></a><br/><br/>その時点まで一旦遡ってから改めて俯瞰すると、最近のWebサービスを巡る再評価の流れもそれがどうしたと斜に構えやすい。XMLも5年くらい前に流行り、当時はXMLスキーマが今後重要になると言われていたが、そのあたりの知識が役に立つ局面が以後拡大したとは思えない。そういうわけで、この辺りの物事は個人的な関心からは長いこと外れていた。2007年の現在それらはつまらない玩具のようなものに留まるのか、それとも個人ユーザにとって真に使える道具なのか。近頃の各種Webサービスは、APIを開放し、プレスリリースを出して客寄せを行い、利用者が増えたら閉じるという、提供企業による手軽な宣伝活動の産物であり、個人ユーザ囲い込みの道具である。それを利用する個人の動機はWeb広告業界に投じられている金で、その量が5年前とは相当異なる以上、以前とは位相が異なってはいるわけだ。Webサービス同士の競争が起こり良質なサービスが数多く提供されるまでになれば面白くなるかもしれない。しかし、Webサービスを介して提供される元ネタが数少ない企業に独占されている現状では、そんなシナリオが実現する蓋然性は低く、バリエーションの少ない似通ったWebサービスを「マッシュアップ」と称してデプロイする個人は、IT企業の走狗として活動しながら細々と広告収入を稼ぐことになる。<br/><br/>今回の試みでは、「<a href="http://www.hatena.ne.jp/" target="_blank">はてな</a>」 の認証WebサービスAPIと、「はてな」のキーワードWebサービスAPIを利用している。1つ目のWebアプリケーションは、「はてな」認証APIを 利用した、「はてな」ユーザが特定の他ユーザに外部サーバを介して任意のデータファイルを渡すためのファイルアップローダである。言語はRubyで、RubyのWebサーバ<a href="http://mongrel.rubyforge.org/" target="_blank">Mongrel</a>のモジュールとして書かれている。もう1つのWebアプリケーションは、はてなキーワードAPIを利用した穴埋めクイズ作成と、はてなキーワード連想グラフの視覚化を行う。言語はPerlで、PerlのWebサーバ<a href="http://search.cpan.org/dist/POE-Component-Server-HTTP/" target="_blank">POE::Component::Server::HTTP</a>のモジュールとして書かれている。<br/><br/>これらは、双方ともスクリプト内でWebサーバモジュールをインクルードするので、コンソールからrubyなりperlなりを通してスクリプトを実行すればそのまま動作し、ホスト用のWebサーバを別途必要としない。起動時に設定ファイルの内容を読んで、以降はWebサーバの一部としてリクエストに応じるWebアプリケーションである。<a href="http://zzz.zggg.com/2006/11/15/perl-ruby-multithreading-embedding/" target="_blank">前回のPerlとRubyの比較</a>ではマルチスレッドと組み込みがポイントだったのに対し、今回は、Webアプリケーションの作成と簡易Webサーバという、Webアプリケーションをめぐる 環境の比較を行うという趣向だ。マルチスレッドに関しては、Mongrelが当然のようにマルチスレッドで、モジュールも対応が必要なのに対し、POEはマルチスレッドに対応していないようである。<br/><br/>双方のWebサーバともプロダクション環境に適した性能を有するサーバではなく、またここで解説するWebアプリケーションはXSS対策やSQLインジェクション対策などのセキュリティ上のケアを欠いているので、あくまで各WebサービスAPIの動作サンプルとしてのみ御覧いただきたい。ソースコードの文字コードは双方ともUTF-8で、Windows XP SP2上にて作成し、Microsoft Internet Explorer 7ならびにFirefox 3.0a1 trunk build 20061218で動作確認している。「はてな」のWeb APIの仕様は本記事を書いた2006年末の時点のものに依っているので、仕様変更によって任意の時点でこれらのアプリケーションが動かなくなっている可能性もある。<br/><br/>では一つ目の、Rubyアプリケーションの方から見ていこう。<code>hatenawebapp1.conf</code>は設定ファイルで、YAMLフォーマットである。Webサーバのポートや、認証APIに与えるキー、データベースファイル名などを設定する。<br/><br/><pre class="brush: ruby"># hatenawebapp1.conf<br/>#<br/># configuration file for hatenawebapp1.rb<br/><br/># Bound address<br/>bound_address: "127.0.0.1"<br/><br/># Bound port<br/>bound_port: 80<br/><br/># API key for Hatena Auth<br/>api_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"<br/><br/># Private key for Hatena Auth<br/>private_key: "xxxxxxxxxxxxxxxx"<br/><br/># File name for the SQLite database<br/>db_filename: "hatenawebapp1.db"<br/><br/># Path for uploaded files<br/>file_store_path: "./store"<br/></pre><br/><br/>以下はスクリプト本体<code>hatenawebapp1.rb</code>で、<code>ruby 1.8.5 (2006-12-04 patchlevel 2) [i386-mswin32]</code>で動作確認した。必要ライブラリは、<br/><code>RubyGems-0.9.0<br/>mongrel-0.3.13.3-mswin32<br/>sqlite3-ruby-1.1.0-mswin32<br/>hatenaapiauth-0.1.0<br/>uuidtools-1.0.0<br/>scrapi-1.2.0</code><br/>と、<a href="http://rubyforge.org/projects/rubygems/" target="_blank">rubygems</a>ツールで取得可能な依存ライブラリである。尚、sqlite3のライブラリのバイナリが別途必要で、Windowsの場合はスクリプトのディレクトリにsqlite3.dllを、Unixの場合も<a href="http://www.sqlite.org/" target="_blank">SQLite公式サイト</a>で入手できるライブラリのバイナリをパスの通った場所へ置く必要がある。また、Mongrelは0.3.13.3を使用しているが、アップデートが頻繁なので異なるバージョンは不具合が出る可能性もある。<br/><br/><pre class="brush: ruby"><br/>=begin<br/><br/>hatenawebapp1.rb<br/><br/>Sample Web Application 1 with Hatena Web Service API : File Transfer between Hatena Users<br/><br/>by RyuK (klassphere[at.mark]gmail.com)<br/>http://zzz.zggg.com/<br/>http://aiueo.da.ru/<br/><br/>[Requirements (tested on Microsoft Windows XP)]<br/><br/>ruby 1.8.5 (2006-12-04 patchlevel 2) [i386-mswin32]<br/><br/>mongrel-0.3.13.3-mswin32<br/>sqlite3-ruby-1.1.0-mswin32<br/>hatenaapiauth-0.1.0<br/>uuidtools-1.0.0<br/>scrapi-1.2.0<br/><br/>... and other dependent Ruby gems<br/><br/>=end<br/><br/>require 'rubygems'<br/><br/>require 'mongrel'<br/>require 'yaml'<br/>require 'sync'<br/>require 'fileutils'<br/>require 'pathname'<br/>require 'sqlite3'<br/>require 'hatena/api/auth'<br/>require 'uuidtools'<br/>require 'scrapi'<br/><br/></pre><br/><br/>YAMLの設定ファイルをロードし、SQLiteデータベースのテーブルを無ければ新規作成する。<br/><br/><pre class="brush: ruby"><br/><br/>conf_filename = $PROGRAM_NAME.clone<br/>begin<br/> $conf = YAML.load_file conf_filename.sub!(/\.rb/, '.conf')<br/><br/> pn = Pathname.new($conf["file_store_path"])<br/> begin<br/> $conf["file_store_path"] = pn.realpath<br/> rescue SystemCallError<br/> Dir.mkdir(pn.to_s, 0701)<br/> $conf["file_store_path"] = pn.realpath<br/> end<br/><br/> $db = SQLite3::Database.new($conf["db_filename"])<br/><br/>rescue Exception => e<br/> STDERR.puts e.to_s<br/> exit(1)<br/>end<br/><br/># ofn = original file name, rfn = real file name<br/><br/>begin<br/> $db.execute(<<SQL<br/>create table files(<br/> sender TEXT,<br/> receiver TEXT,<br/> ofn TEXT,<br/> rfn TEXT,<br/> date INTEGER,<br/> size INTEGER<br/>);<br/>SQL<br/> )<br/>rescue SQLite3::SQLException => e<br/> if e.to_s != "table files already exists"<br/> puts e.to_s<br/> exit(1)<br/> end<br/>end<br/><br/>$db.extend(Sync_m)<br/><br/></pre><br/><br/>Webサーバのモジュールなので、データベースアクセスのためのグローバルオブジェクト$dbはSync_mを使用してマルチスレッド対応にしておく。アップロードされてくるファイルのファイルサイズと、受け取り済みのデータのサイズとを保存するために、<code>Struct</code>クラスを使って<code>DownloadProgress</code>という構造体を作っておく。さらに、<code>$download_progress</code>という<code>Hash</code>のオブジェクトを生成し、このオブジェクトが<code>QUERY_STRING</code>リクエストパラメータと<code>DownloadProgress</code>オブジェクトとの対応表を保存する。<code>$download_progress</code>は、デザインパターンで言うところのObserverパターンで、ダウンロード状況のview(MVCの'V')としてWebからの複数リクエストによって同時に参照される可能性があるため、同じようにマルチスレッド対応にしなければならない。<br/><br/><pre class="brush: ruby"><br/><br/>$verified_users_ipaddress = Hash.new # ip - user<br/>$verified_users_ipaddress.extend(Sync_m)<br/><br/>$existent_users = Hash.new # user - dummy<br/>$existent_users.extend(Sync_m)<br/><br/>DownloadProgress = Struct.new("DownloadProgress", :total_size, :current_size)<br/><br/>$download_progress = Hash.new<br/>$download_progress.extend(Sync_m) # user - DP<br/><br/></pre><br/><br/>「はてな」の認証APIの初期化関数を呼び出し、次いで表示するページ内のヘッダをそのままヒアドキュメントを使ってスクリプト内に書いている。<br/><br/><pre class="brush: ruby"><br/><br/>$hatena_auth = Hatena::API::Auth.new(:api_key => $conf["api_key"], :secret => $conf["private_key"])<br/><br/>$page_head =<<EOS<br/><!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><br/><html><br/><head><br/><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><br/><title>#{$PROGRAM_NAME}</title><br/></head><br/><body><br/><p>Proof of Concept: File Transfer between Hatena Users</p><br/>EOS<br/><br/>$page_end =<<EOS<br/></body><br/></html><br/>EOS<br/><br/>def page_head_with_onload(onload)<br/> <<EOS<br/><!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><br/><html><br/><head><br/><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><br/><title>#{$PROGRAM_NAME}</title><br/></head><br/><body onload="#{onload}"><br/><p>Proof of Concept: File Transfer between Hatena Users</p><br/>EOS<br/><br/>end<br/><br/></pre><br/><br/>以下に、Mongrelのハンドラ関数群が続く。各種デフォルトハンドラをオーバーライドすることにより、WebアプリケーションはMongrelの動作をカスタマイズするというのがMongrelモジュールの基本コンセプトである。まずは、ユーザが最初にサーバのルートパス('/')にアクセスしたときに、「はてな」のIDとパスワードを認証APIに対して差し出すように促す。<code>process</code>というメソッドが ユーザ定義フィルタの役割を果たし、<code>request</code>を受けて<code>response</code>を返す。<br/><br/><pre class="brush: ruby"><br/><br/># Mongrel handlers #########################################<br/><br/>class RootHandler < Mongrel::HttpHandler<br/> def process(request, response)<br/> response.start do |head, out|<br/> head["Content-Type"] = "text/html"<br/><br/> out << <<EOS<br/>#{$page_head}<br/><br/><p>If you are a Hatena user and want to transfer a file to another user,<br/><a href=\"#{$hatena_auth.uri_to_login}\">please follow this link</a> to the uploader.</p><br/><br/>#{$page_end}<br/>EOS<br/> end<br/> end<br/>end<br/><br/></pre><br/><br/>このスクリプトの一番最後にMongrel起動時の初期設定を行う部分があるので、そこを見てもらうとして、この<code>UploaderHandler</code>は、"/uploader" というパスにアクセスした場合のハンドラである。リクエスト中のクエリ文字列(<code>request.params["QUERY_STRING"]</code>)を解析し、認証情報 を取り出して「はてな」認証APIに与える。「はてな」ユーザとして認証されると、<code>$verified_users_ipaddress</code>ハッシュ表にリモートIPアドレスとユーザ名の組が保存される。ちなみに、このアプリケーションはIPアドレス1つあたり1ユーザとして認識しており、まともなセッション管理を行っていないので、実際に使用するには厳密なセッション管理が必要である。<br/><br/><pre class="brush: ruby"><br/><br/>class UploaderHandler < Mongrel::HttpHandler<br/> def show_error(response, message)<br/> response.start do |head, out|<br/> head["Content-Type"] = "text/html"<br/> out << message<br/> end<br/> end<br/><br/> def process(request, response)<br/> unknown_error_page =<<EOS<br/>#{$page_head}<br/><br/><p>Error: Unknown Error</p><br/><script language="JavaScript"><br/><!--<br/>history.go(-1)<br/>//--><br/></script><br/><br/>#{$page_end}<br/>EOS<br/><br/> auth_error_page =<<EOS<br/>#{$page_head}<br/><br/><p>Error: Authorization Failed</p><br/><br/>#{$page_end}<br/>EOS<br/><br/> cert_key = ""<br/> if request.params["QUERY_STRING"] =~ /cert=([^&amp;]+)/<br/> cert_key = $1<br/> else<br/> show_error(response, unknown_error_page)<br/> return<br/> end<br/><br/> user = nil<br/> begin<br/> user = $hatena_auth.login(cert_key)<br/> rescue Hatena::API::AuthError<br/> show_error(response, auth_error_page)<br/> return<br/> end<br/><br/> $verified_users_ipaddress.synchronize() do<br/> if $verified_users_ipaddress.size > 10000<br/> $verified_users_ipaddress.clear<br/> end<br/> $verified_users_ipaddress[request.params[Mongrel::Const::REMOTE_ADDR]] = user['name']<br/> end<br/><br/></pre><br/><br/>このページのヘッダはJavaScriptを含んでいて、AJAXの簡単なフレームワークと、「はてな」ユーザ名の実在性を確かめるメソッド、ファイルのアップロード状態をポーリングしながら進捗バーを動的に表示するメソッドなどを含む。AJAXによる画面遷移無しのファイルアップロードを実現するために、ファイルのアップロード先を隠しiframeにするというテクニックが使用されている。<br/><br/><pre class="brush: ruby"><br/> response.start do |head, out|<br/> head["Content-Type"] = "text/html"<br/><br/> results =<<EOS<br/>#{page_head_with_onload("setup('#{user['name']}')")}<br/><script language="JavaScript"><br/><!--<br/><br/>var isMozilla = navigator.userAgent.indexOf('Gecko') != -1;<br/>var isIE = window.ActiveXObject;<br/><br/>function createHttpRequest()<br/>{<br/> if (isIE)<br/> {<br/> try<br/> { // CLSID_XMLHTTP<br/> // v 3.0<br/> return new ActiveXObject("Msxml2.XMLHTTP");<br/> }<br/> catch (e)<br/> {<br/> try<br/> {// v 2.x<br/> return new ActiveXObject("Microsoft.XMLHTTP");<br/> }<br/> catch (e2)<br/> {<br/> return null;<br/> }<br/> }<br/> }<br/> else if (window.XMLHttpRequest) // non-IE<br/> {<br/> var hr = new XMLHttpRequest();<br/> if (isMozilla)<br/> hr.overrideMimeType('text/xml');<br/> return hr;<br/> }<br/> else<br/> {<br/> return null;<br/> }<br/>}<br/><br/>function sendHTTP(data, method, uri, callback, async, caller)<br/>{<br/> var hr = createHttpRequest();<br/><br/> var args = new Array();<br/> args.push(hr);<br/> for (var i = 6; i < arguments.length; ++i)<br/> {<br/> args.push(arguments[i]);<br/> }<br/><br/> try<br/> {<br/> hr.open(method, uri, async);<br/> hr.setRequestHeader("If-Modified-Since", "Thu, 01 Jun 1970 00:00:00 GMT");<br/><br/> hr.onreadystatechange = function()<br/> {<br/> if (hr.readyState == 4)<br/> {<br/> callback.apply(caller, args);<br/> }<br/> }<br/><br/> hr.send(data);<br/> delete hr;<br/> }<br/> catch(e)<br/> {<br/> alert("sendHTTP: " + e);<br/> }<br/>}<br/><br/>function setup(owner_name)<br/>{<br/> loadFileList(owner_name);<br/> checkUsername();<br/>}<br/><br/>var filename = "";<br/>var query_progress = "";<br/>var time_start = 0;<br/><br/>function startPolling(form)<br/>{<br/> if (form.filename.value == "")<br/> {<br/> alert("Invalid file name");<br/> return;<br/> }<br/><br/> if (form.receiver.value == "")<br/> {<br/> alert("Invalid user name");<br/> return;<br/> }<br/><br/> var x = document.getElementById("hidden_div");<br/> if (x)<br/> document.body.removeChild(x);<br/><br/> var d = document.createElement('div');<br/> d.setAttribute("id", "hidden_div");<br/> d.innerHTML='<iframe id="hidden_iframe" name="hidden_iframe" style="display: none; width: 0px; height: 0px; border: 0px"></iframe>';<br/> document.body.appendChild(d);<br/><br/> form.button_upload.disabled = true;<br/> filename = form.filename.value;<br/><br/> document.getElementById("form_area").innerHTML = ("<p>Uploading <b>" + filename + "</b></p>");<br/><br/> query_progress = (form.sender.value + '&amp;' + form.receiver.value + '&amp;' + filename);<br/><br/> form.action = ("/receiver?" + query_progress);<br/><br/> var d = new Date();<br/> time_start = d.getTime();<br/><br/> form.submit();<br/><br/> pollDownloadProgress();<br/>}<br/><br/></pre><br/><br/><code>pollDownloadProgress</code>関数を1秒毎にタイマー呼び出しして/query_progressへAJAX問い合わせを行い、 ダウンロード済みのファイルサイズを更新しつつ進捗バーを伸ばす。<br/><br/><pre class="brush: js"><br/><br/>function pollDownloadProgress()<br/>{<br/> sendHTTP('', 'POST', './query_progress' + '?' + query_progress, pollDownloadProgressCallback, true);<br/><br/> if (filename != "")<br/> setTimeout("pollDownloadProgress();", 1000);<br/>}<br/><br/>function pollDownloadProgressCallback(hr)<br/>{<br/> var nodelist = hr.responseXML.getElementsByTagName("f");<br/> if (!nodelist || nodelist.length == 0)<br/> return;<br/><br/> var current_size = 0;<br/> var total_size = 0;<br/><br/> for (var i = 0; i < nodelist.length; ++i)<br/> {<br/> var n = nodelist.item(i);<br/> if (n == null)<br/> return;<br/><br/> var nn = n.firstChild;<br/> if (nn == null)<br/> return;<br/><br/> while (nn != null)<br/> {<br/> // nn.textContent == Mozilla only<br/> // NODE_TEXT == 3 || NODE_CDATA_SECTION == 4<br/> if (nn != null &amp;&amp; (nn.nodeType == 3 || nn.nodeType == 4))<br/> {<br/> if (nn.nodeValue != "")<br/> {<br/> var matched = nn.nodeValue.match(/(\\d+)\\/(\\d+)/);<br/> if (matched)<br/> {<br/> current_size = parseInt(matched[1], 10);<br/> total_size = parseInt(matched[2], 10);<br/><br/> setProgressBar(current_size, total_size);<br/> }<br/> }<br/> }<br/><br/> nn = nn.nextSibling;<br/> }<br/> }<br/>}<br/><br/>function setProgressBar(received_size, total_size)<br/>{<br/> document.getElementById("d2").style.width = 400 * (received_size / total_size) + "px";<br/><br/> var d = new Date();<br/> var elapsed_time = (d.getTime() - time_start) / 1000;<br/> if (elapsed_time <= 0)<br/> elapsed_time = 1;<br/><br/> var speed = parseInt(received_size / elapsed_time / 1024, 10);<br/><br/> if (400 * (received_size / total_size) >= 75)<br/> document.getElementById("d1").innerHTML<br/> = "<font size=-1>" + parseInt(received_size / total_size * 100) + "% (" + speed.toString() + "KB/s)</font>";<br/>}<br/><br/></pre><br/><br/>"/check_username"というパスに、あるユーザ名が実在の「はてな」ユーザかどうか確かめるサービス(後述の<code>CheckUsernameHandler</code>)が動いているので、そこに対してAJAXで問い合わせを行う。<br/><br/><pre class="brush: js"><br/><br/>var checked_user = "";<br/>var check_username_requesting = false;<br/><br/>// onchange doesn't work until the focus is out<br/>function checkUsername()<br/>{<br/> if (document.getElementById("receiver").value != ""<br/> &amp;&amp; document.getElementById("receiver").value != checked_user &amp;&amp; !check_username_requesting)<br/> {<br/> check_username_requesting = true;<br/> checked_user = document.getElementById("receiver").value;<br/> document.getElementById("form_area").innerHTML = ("<p><blink>Checking if <b"<br/> + checked_user + "</b> is an existing Hatena user...</blink></p>");<br/> sendHTTP(checked_user, 'POST', './check_username', checkUsernameCallback, true);<br/> }<br/><br/> setTimeout(checkUsername, 3000);<br/>}<br/><br/>function isNotUser()<br/>{<br/> document.getElementById("form_area").innerHTML = ("<p><b>" + checked_user + "</b> is not a Hatena user.</p>");<br/> document.getElementById("button_upload").disabled = true;<br/>}<br/><br/>function checkUsernameCallback(hr)<br/>{<br/> // Can't use responseXML.getElementById() -<br/> // For responseXML.getElementById() to return an element with the matching id value, XMLHttpRequest<br/> // implementations must be aware of the underlying schema/DTD that defines an id attribute of type ID.<br/> // Currently browsers are not schema/DTD aware for XMLHttpRequests, although they support well-known DTDs<br/> // like HTML and XHTML for documents.<br/><br/> var nodelist = hr.responseXML.getElementsByTagName("r");<br/> if (!nodelist || nodelist.length == 0)<br/> isNotUser();<br/><br/> check_username_requesting = false;<br/> for (var i = 0; i < nodelist.length; ++i)<br/> {<br/> var n = nodelist.item(i);<br/> if (n == null)<br/> {<br/> isNotUser();<br/> continue;<br/> }<br/><br/> var nn = n.firstChild;<br/> if (nn == null)<br/> {<br/> isNotUser();<br/> continue;<br/> }<br/><br/> while (nn != null)<br/> {<br/> // nn.textContent == Mozilla only<br/> // NODE_TEXT == 3 || NODE_CDATA_SECTION == 4<br/> if (nn != null &amp;&amp; (nn.nodeType == 3 || nn.nodeType == 4))<br/> {<br/> if (nn.nodeValue != "")<br/> {<br/> document.getElementById("button_upload").disabled = false;<br/> checked_user = nn.nodeValue;<br/> document.getElementById("receiver").value = nn.nodeValue;<br/> document.getElementById("form_area").innerHTML = ("<p><b>" + checked_user + "</b> is an existing Hatena user.</p>");<br/> }<br/> }<br/><br/> nn = nn.nextSibling;<br/> }<br/> }<br/><br/> check_username_requesting = false;<br/>}<br/><br/>function loadFileList(owner_name)<br/>{<br/> sendHTTP('', 'POST', './list_files' + '?' + owner_name, loadFileListCallback, true);<br/>}<br/><br/>function unixtime2localdate(t)<br/>{<br/> var d = new Date;<br/> d.setTime(t * 1000);<br/> return d.toLocaleString();<br/>}<br/><br/>function loadFileListCallback(hr)<br/>{<br/> var out = "";<br/><br/> var nodelist = hr.responseXML.getElementsByTagName("myfile");<br/> if (nodelist)<br/> {<br/> out += "<p>Your files sent to other users:</p>";<br/> for (var i = 0; i < nodelist.length; ++i)<br/> {<br/> var e = nodelist.item(i);<br/> out += "<p><a href='./downloader/";<br/> out += e.getAttribute("rfn");<br/> out += "'><b>";<br/> out += e.getAttribute("ofn");<br/> out += "</b></a> (Size: ";<br/> out += Math.floor(parseInt(e.getAttribute("size"), 10) / 1024).toString();<br/> out += "KB - To: <a target='_blank' href='http://www.hatena.ne.jp/user?userid=";<br/> out += e.getAttribute("r");<br/> out += "'>";<br/> out += e.getAttribute("r");<br/> out += "</a> - Date: ";<br/> out += unixtime2localdate(parseInt(e.getAttribute("d"), 10));<br/> out += ")</p>";<br/> }<br/> if (nodelist.length == 0)<br/> out += "<p>(no files)</p>";<br/> }<br/><br/> nodelist = hr.responseXML.getElementsByTagName("sentfile");<br/> if (nodelist)<br/> {<br/> out += "<p>Files sent to you from other users:</p>";<br/> for (var i = 0; i < nodelist.length; ++i)<br/> {<br/> var e = nodelist.item(i);<br/> out += "<p><a href='./downloader/";<br/> out += e.getAttribute("rfn");<br/> out += "'><b>";<br/> out += e.getAttribute("ofn");<br/> out += "</b></a> (Size: ";<br/> out += Math.floor(parseInt(e.getAttribute("size"), 10) / 1024).toString();<br/> out += "KB - From: <a target='_blank' href='http://www.hatena.ne.jp/user?userid=";<br/> out += e.getAttribute("s");<br/> out += "'>";<br/> out += e.getAttribute("s");<br/> out += "</a> - Date: ";<br/> out += unixtime2localdate(parseInt(e.getAttribute("d"), 10));<br/> out += ")</p>";<br/> }<br/> if (nodelist.length == 0)<br/> out += "<p>(no files)</p>";<br/> }<br/><br/> document.getElementById("files_list").innerHTML = out;<br/>}<br/><br/>//--><br/></script><br/><br/><p>Welcome <b>#{user['name']}</b> @ Hatena.</p><br/><p>You are now authorized to upload a file to transfer to another Hatena user.</p><br/><br/><span id="form_area"><p>Fill in the name of the receiving user and upload a file.</p></span><br/><br/><p><br/><!-- the colon at the end of startPolling() is required. --><br/><form method="POST" id="file_form" action="" enctype="multipart/form-data"<br/> target="hidden_iframe" onsubmit="startPolling(this); return false;"><br/><input type="hidden" name="sender" value="#{user['name']}"><br/>Receiving Hatena User: <input type="text" id="receiver" name="receiver" size="16"><br><br><br/>File to upload: <input type="file" name="filename" size="80"><br/><input type="submit" id="button_upload" value="Upload this file" disabled><br/></form><br/></p><br/><br/><p><br/><div id="empty" style="background-color: #cccccc; border: 1px solid black; height: 30px; width: 400px; padding: 0px;" align="left"/><br/><div id="d2" style="position: relative; top: 0px; left: 0px; background-color: #333333;<br/>height: 30px; width: 0px; padding-top: 5px; padding: 0px;"/><br/><div id="d1" style="position: relative; top: 0px; left: 0px; color: #f0ffff; height: 30px; text-align: center; font: bold;<br/> padding: 0px; padding-top: 5px;"/></div></div></div><br/></p><br/><br/><span id="files_list"></span><br/><br/>#{$page_end}<br/>EOS<br/> out << results<br/> end<br/> end<br/>end<br/></pre><br/><br/>"/downloader"でアクセスできる、他ユーザが自分宛にアップロードしたファイルを受け取りダウンロードするためのURLのハンドラを定義する。<br/><br/><pre class="brush: ruby"><br/>class DownloaderHandler < Mongrel::DirHandler<br/> def initialize(path, listing_allowed=true, index_html="index.html")<br/> super(path, listing_allowed, index_html)<br/><br/> #Mongrel::DirHandler::add_mime_type(".zip", "application/zip")<br/> #Mongrel::DirHandler::add_mime_type(".rar", "application/x-rar-compressed")<br/> #Mongrel::DirHandler::add_mime_type(".lzh", "application/x-lzh");<br/> #Mongrel::DirHandler::add_mime_type(".xml", "text/xml");<br/> end<br/><br/> def process(request, response)<br/> user = nil<br/> $verified_users_ipaddress.synchronize(Sync_m::SH) do<br/> if $verified_users_ipaddress.include?(request.params[Mongrel::Const::REMOTE_ADDR])<br/> user = $verified_users_ipaddress[request.params[Mongrel::Const::REMOTE_ADDR]]<br/> end<br/> end<br/><br/> unless user<br/> response.reset<br/> response.start do |head, out|<br/> head["Content-Type"] = "text/html"<br/> out << "Authorization Failed - <a href=\"#{$hatena_auth.uri_to_login}\">Verify again</a>"<br/> end<br/> return<br/> end<br/><br/> req_method = request.params[Mongrel::Const::REQUEST_METHOD] || Mongrel::Const::GET<br/> req_path = can_serve request.params[Mongrel::Const::PATH_INFO]<br/><br/> if not req_path<br/> response.reset<br/> response.start(404) do |head, out|<br/> out << "File not found"<br/> end<br/> else<br/> original_filename = ""<br/> real_filename = request.params[Mongrel::Const::PATH_INFO].clone<br/> real_filename.gsub!("/", "");<br/><br/> if real_filename =~ /[^a-zA-Z0-9_-\.]/ || user =~ /[^a-zA-Z0-9_-]/<br/> response.reset<br/> response.start(403) do |head, out|<br/> out << "Invalid Request"<br/> end<br/> return<br/> end<br/><br/> $db.synchronize(Sync_m::SH) do<br/> $db.execute("select * from files where rfn = '#{real_filename}' AND receiver = '#{user}'") do |row|<br/> original_filename = row[2]<br/> end<br/> end<br/><br/> if original_filename == ""<br/> response.reset<br/> response.start(403) do |head, out|<br/> out << "Not Authorized"<br/> end<br/> return<br/> end<br/><br/> response.header["Content-Disposition"] = "filename=\"#{original_filename}\"";<br/> begin<br/> if req_method == Mongrel::Const::HEAD<br/> send_file(req_path, request, response, true)<br/> elsif req_method == Mongrel::Const::GET<br/> send_file(req_path, request, response, false)<br/> else<br/> response.start(403) {|head, out| out.write(Mongrel::ONLY_HEAD_GET) }<br/> end<br/> rescue => details<br/> STDERR.puts "Error sending file #{req_path}: #{details}"<br/> end<br/> end<br/> end<br/>end<br/></pre><br/><br/>"/receiver"のパスが、ユーザがファイルをアップロードする対象である。 <code>request_progress</code>メソッドはリクエストのデータを一定量受け取る度にMongrelが呼び出すイベントコールバックで、これをオーバーライドすることによって、アップロードされてくるファイルのアップロード済みデータ量の数値を逐次更新する。<code>Mongrel::CGIWrapper</code>を 使用してHTMLフォームから送信されてくるデータを解析しているが、使用したMongrelのバージョンではファイルをアップロードしている場合に正常 にそれぞれのクエリ要素を受け取れないというバグがあるので、迂回策として生のデータを正規表現で検索している。ファイルのダウンロードが済むと、<code>Mongrel::CGIWrapper</code>の仕様に従って、アップロードされてきたファイルのサイズに応じ、一時バッファもしくは一時ファイルから、実際のファイル保存先へとデータを移す。<br/><br/><pre class="brush: ruby"><br/>class ReceiverHandler < Mongrel::HttpHandler<br/> def initialize<br/> @request_notify = true<br/> end<br/><br/> def request_progress(params, clen, total)<br/> $download_progress.synchronize() do<br/> if $download_progress.size > 1000<br/> $download_progress.clear<br/> end<br/> $download_progress[params["QUERY_STRING"]] = Struct::DownloadProgress.new(total, total - clen)<br/> end<br/> end<br/><br/> def gen_stored_filename()<br/> return UUID.timestamp_create.to_s<br/> end<br/><br/> def process(request, response)<br/> cgi = Mongrel::CGIWrapper.new(request, response)<br/><br/> # can't use cgi["sender"] and cgi["receiver"] for multipart/form-data due to a possible bug of Mongrel 0.3.13.3<br/><br/> sender = nil<br/> receiver = nil<br/> original_filename = nil<br/><br/> if request.params["QUERY_STRING"] =~ /^([^&amp;]+)&amp;([^&amp;]+)&amp;(.+)/<br/> sender = $1<br/> receiver = $2<br/> original_filename = $3<br/> original_filename.gsub!("'", "")<br/> original_filename.gsub!(/.*\\/, "")<br/> original_filename.gsub!(".*/", "")<br/> end<br/><br/> if sender =~ /[^a-zA-Z0-9_-]/ || receiver =~ /[^a-zA-Z0-9_-]/ || original_filename =~ /[^a-zA-Z0-9_-\.\\]/<br/> response.reset<br/> response.start(403) do |head, out|<br/> out << "Invalid Request"<br/> end<br/> return<br/> end<br/><br/> $verified_users_ipaddress.synchronize(Sync_m::SH) do<br/> if $verified_users_ipaddress.include?(request.params[Mongrel::Const::REMOTE_ADDR])<br/> if sender != $verified_users_ipaddress[request.params[Mongrel::Const::REMOTE_ADDR]]<br/> sender = nil<br/> end<br/> else<br/> sender = nil<br/> end<br/> end<br/><br/> $existent_users.synchronize(Sync_m::SH) do<br/> unless $existent_users.include?(receiver)<br/> receiver = nil<br/> end<br/> end<br/><br/> unless sender &amp;&amp; receiver &amp;&amp; original_filename<br/> response.start do |head, out|<br/> head["Content-Type"] = "text/html"<br/><br/> out << <<EOS<br/>#{$page_head}<br/><br/><script language="JavaScript"><br/><!--<br/><br/>parent.document.getElementById("form_area").innerHTML<br/> = ("<p>An authorization error happened in uploading <b>" + parent.filename<br/> + "</b>. <a href=\\"#{$hatena_auth.uri_to_login}\\">Please retry</a></p>");<br/>parent.filename = "";<br/>parent.setProgressBar(0, 0);<br/><br/>//--><br/></script><br/><br/>#{$page_end}<br/>EOS<br/> end<br/> return<br/> end<br/><br/> $download_progress.synchronize() do<br/> $download_progress.delete(request.params["QUERY_STRING"])<br/> end<br/><br/> file = cgi['filename']<br/><br/> real_filename = gen_stored_filename()<br/><br/> begin<br/> if file.size >= 10240 then<br/> FileUtils.cp(file.path, $conf["file_store_path"] + real_filename)<br/> else<br/> open($conf["file_store_path"] + real_filename, "wb") do |fh|<br/> fh.write(file.read)<br/> end<br/> end<br/><br/> $db.synchronize() do<br/> $db.transaction do |d|<br/> d.execute("insert into files values('#{sender}', '#{receiver}',<br/> '#{original_filename}', '#{real_filename}', #{Time.now.tv_sec}, #{file.size})")<br/> end<br/> end<br/> rescue Exception => e<br/> p e<br/> puts e.backtrace<br/> end<br/><br/> response.start do |head, out|<br/> head["Content-Type"] = "text/html"<br/><br/> out << <<EOS<br/>#{$page_head}<br/><br/><script language="JavaScript"><br/><!--<br/><br/>parent.document.getElementById("form_area").innerHTML = ("<p><b>"<br/>+ parent.filename + "</b> has been successfully uploaded.</p>");<br/>parent.filename = "";<br/>parent.setProgressBar(#{file.size}, #{file.size});<br/>parent.loadFileList('#{sender}');<br/>parent.document.getElementById("button_upload").disabled = false;<br/><br/>//--><br/></script><br/><br/>#{$page_end}<br/>EOS<br/> end<br/> end<br/>end<br/><br/></pre><br/><br/><code>scrapi</code>のWindows版で1モジュールのdllの読み込みに不具合があるので、問題になるメソッド<code>find_tidy</code>をここで上書き修正している。この辺は動的言語の面目躍如と言うべきだが、濫用すると収拾が付かなくなるので個人的にはやむをえない場合以外やるべきではないと思っている。<br/><br/><pre class="brush: ruby"><br/><br/># Redefine the find_tidy method in scrapi for Windows to load the correct dll first<br/>module Scraper<br/> module Reader<br/> module_function<br/><br/> def find_tidy()<br/> return if Tidy.path<br/><br/> begin<br/> $LOAD_PATH.each do |path|<br/> if path =~ /scrapi/ &amp;&amp; path =~ /lib$/<br/> if Config::CONFIG['arch'] =~ /mswin/<br/> Tidy.path = File.join(path, "/tidy", "libtidy.dll")<br/> else<br/> Tidy.path = File.join(path, "/tidy", "libtidy.so")<br/> end<br/> break<br/> end<br/> end<br/> rescue LoadError => e<br/> puts e.to_s<br/> end<br/> end<br/> end<br/>end<br/><br/></pre><br/><br/>あるユーザ名が「はてな」の実在のユーザかどうか確かめるためのwebサービスAPIを「はてな」では提供していないので、「はてな」上に該当ユーザのメンバーページが存在するかどうか、<code>scrapi</code>によるスクレイピングを行って強引に確かめることで代用する。<br/><br/><pre class="brush: ruby"><br/><br/>class CheckUsernameHandler < Mongrel::HttpHandler<br/><br/> @@scraper = Scraper.define do<br/> process "td[align='center'] a[href='/q']", :ret => :text<br/> result :ret<br/> end<br/><br/> def process(request, response)<br/> verified = false<br/> $verified_users_ipaddress.synchronize(Sync_m::SH) do<br/> if $verified_users_ipaddress.include?(request.params[Mongrel::Const::REMOTE_ADDR])<br/> verified = true<br/> end<br/> end<br/><br/> unless verified<br/> response.start(403) do |head, out|<br/> out.write("Not Authorized")<br/> end<br/> return<br/> end<br/><br/> found = false<br/> $existent_users.synchronize(Sync_m::SH) do<br/> found = $existent_users.include?(request.body.string)<br/> end<br/><br/> unless found<br/> uri = "http://www.hatena.ne.jp/user?userid="<br/> uri += request.body.string # StringIO<br/><br/> found = (@@scraper.scrape(URI.parse(uri)) == nil)<br/><br/> $existent_users.synchronize() do<br/> if $existent_users.size > 1000<br/> $existent_users.clear()<br/> end<br/> $existent_users[request.body.string] = 1<br/> end<br/> end<br/><br/> response.start do |head, out|<br/> head["Content-Type"] = "text/xml"<br/> out.write(<br/> "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><r>#{found ? request.body.string : ""}</r>")<br/> end<br/> end<br/>end<br/><br/></pre><br/><br/>自分がアップロード中のファイルの進捗状況を示すXMLを返すハンドラを定義する。<br/><br/><pre class="brush: ruby"><br/>class QueryProgressHandler < Mongrel::HttpHandler<br/> def process(request, response)<br/> $verified_users_ipaddress.synchronize(Sync_m::SH) do<br/> unless $verified_users_ipaddress.include?(request.params[Mongrel::Const::REMOTE_ADDR])<br/> response.start(403) do |head, out|<br/> out.write("Not Authorized")<br/> end<br/> return<br/> end<br/> end<br/><br/> found = false<br/> $download_progress.synchronize(Sync_m::SH) do<br/> if $download_progress.include?(request.params["QUERY_STRING"])<br/> found = $download_progress[request.params["QUERY_STRING"]]<br/> end<br/> end<br/><br/> response.start do |head, out|<br/> head["Content-Type"] = "text/xml"<br/> if found<br/> out.write(<br/> "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><f>#{found.current_size}/#{found.total_size}</f>")<br/> else<br/> out.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><f></f>")<br/> end<br/> end<br/> end<br/>end<br/></pre><br/><br/>自分宛に他ユーザがアップロードしたファイルの一覧を表示するハンドラを定義する。<br/><br/><pre class="brush: ruby"><br/>class ListFilesHandler < Mongrel::HttpHandler<br/> def process(request, response)<br/> $verified_users_ipaddress.synchronize(Sync_m::SH) do<br/> if $verified_users_ipaddress.include?(request.params[Mongrel::Const::REMOTE_ADDR])<br/> if $verified_users_ipaddress[request.params[Mongrel::Const::REMOTE_ADDR]] != request.params["QUERY_STRING"]<br/> response.start(403) do |head, out|<br/> out.write("Not Authorized")<br/> end<br/> return<br/> end<br/> else<br/> response.start(403) do |head, out|<br/> out.write("Not Authorized")<br/> end<br/> return<br/> end<br/> end<br/><br/> if request.params["QUERY_STRING"] =~ /[^a-zA-Z0-9_-\.\\]/<br/> response.reset<br/> response.start(403) do |head, out|<br/> out << "Invalid Request"<br/> end<br/> return<br/> end<br/><br/> xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><files>'<br/><br/> $db.synchronize(Sync_m::SH) do<br/> $db.execute("select * from files where sender = '#{request.params["QUERY_STRING"]}'") do |row|<br/> xml += "<myfile r=\"#{row[1]}\" ofn=\"#{row[2]}\" rfn=\"#{row[3]}\" d=\"#{row[4]}\" size=\"#{row[5]}\"/>"<br/> end<br/> $db.execute("select * from files where receiver = '#{request.params["QUERY_STRING"]}'") do |row|<br/> xml += "<sentfile s=\"#{row[0]}\" ofn=\"#{row[2]}\" rfn=\"#{row[3]}\" d=\"#{row[4]}\" size=\"#{row[5]}\"/>"<br/> end<br/> end<br/><br/> xml += "</files>"<br/><br/> response.start do |head, out|<br/> head["Content-Type"] = "text/xml"<br/> out.write(xml)<br/> end<br/> end<br/>end<br/><br/></pre><br/><br/>Mongrelの起動設定と起動、終了処理。どのパス(URI)がどのハンドラクラスによって定義されているか、ここで指定する。<br/><br/><pre class="brush: ruby"><br/><br/>#stats = Mongrel::StatisticsFilter.new(:sample_rate => 1)<br/><br/># new(defaults={}, &amp;blk)<br/># You pass in initial defaults and then a block to continue configuring.<br/>config = Mongrel::Configurator.new :host => $conf["bound_address"], :port => $conf["bound_port"] do<br/> listener do<br/> uri "/", :handler => RootHandler.new<br/> #uri "/", :handler => Mongrel::DeflateFilter.new # This messes up IE<br/> #uri "/", :handler => stats<br/><br/> uri "/uploader", :handler => UploaderHandler.new<br/><br/> uri "/downloader", :handler => DownloaderHandler.new($conf["file_store_path"], false)<br/><br/> uri "/receiver", :handler => ReceiverHandler.new<br/><br/> uri "/check_username", :handler => CheckUsernameHandler.new<br/><br/> uri "/query_progress", :handler => QueryProgressHandler.new<br/><br/> uri "/list_files", :handler => ListFilesHandler.new<br/><br/> #uri "/status", :handler => Mongrel::StatusHandler.new(:stats_filter => stats)<br/> end<br/><br/> trap("INT") { stop }<br/> run<br/>end<br/><br/>puts "Mongrel running on #{$conf["bound_address"]}:#{$conf["bound_port"]}"<br/><br/>config.join<br/><br/></pre><br/><br/><code>hatenawebapp1.rb</code>は以上である。 スクリプトを動作させると、Mongrelが設定ファイル内のポートで起動するので、Webブラウザでアクセスすると、「はてな」認証を促すリンクが表示される。それをクリックし、認証を通過すると、コールバックURLのhttp://127.0.0.1/uploaderに転送され、そこでファイルのアップロードが可能となる。自分の送信済みファイルと、自分宛に他ユーザが送信したファイルのリストもそこに表示されている。ファイルをアップロードする と、AJAXを利用して画面遷移無しで進捗表示とアップロード完了後のリスト更新が行われる。尚、ごく稀に特定のファイルでアップロードが失敗することがあるようだが、Mongrelの<code>CGIWrapper</code>のバグに起因する問題でありMongrel側の修正を待つしかない。<br/><br/>つぎに、Perl webアプリの方を見ていくことにする。まずは、設定ファイルの<code>hatenawebapp2.conf</code>である。Rubyアプリの方と同様に、YAML形式を用いてサーバのポートなどを設定している。<br/><br/><pre class="brush: ruby"><br/># hatenawebapp2.conf<br/>#<br/># configuration file for hatenawebapp2.pl<br/><br/># Bound port<br/>bound_port: 80<br/><br/># JavaScript directory name (not path)<br/>javascript_directory: jsdir<br/><br/># Mask for a hidden text in a quiz question<br/>quiz_mask: "<font color=red>******</font>"<br/></pre><br/><br/>スクリプト本体は<code>hatenawebapp2.pl</code>である。<code>perl, v5.8.8 built for MSWin32-x86-multi-thread</code>で動作を確認している。<br/><br/>必要ライブラリは、Perl 5.8の他に、<br/><code>YAML::Syck<br/>POE::Component::Server::HTTP<br/>HTTP::Status<br/>XML::RSS<br/>XMLRPC::Lite<br/>LWP::Simple<br/>MeCab<br/>URI::Escape<br/>threads::shared<br/>Thread::Semaphore</code><br/>のそれぞれCPANシェルを使って入手できる最新バージョンと、各々が依存するライブラリである。オープンソース形態素解析エンジン<a href="http://mecab.sourceforge.net/" target="_blank">MeCab</a>は、<a href="http://namazu.asablo.jp/blog/2006/07/12/442318" target="_blank">ナマズのブログ</a>で入手可能な0.92のWindows用バイナリと辞書を使用させていただいた。尚、Windows下ではMeCabがShiftJISでビルドされている ため辞書もShiftJIS版を使用し、スクリプト内で必要な変換を行ったが、他プラットフォームでテストする場合はMeCab、辞書ともUTF-8版が必要である。また、JavaScriptのグラフ視覚化ライブラリである<a href="http://www.jsviz.org/" target="_blank">JSViz</a>と、ツールチップライブラリ<a href="http://boxover.swazz.org/" target="_blank">boxover</a>を利用しており、これらはスクリプト下に<code>jsdir</code>という名称のディレクトリを作ってその中へ全て展開する必要がある。<br/><br/><pre class="brush: perl"><br/><br/>=pod<br/><br/>hatenawebapp2.pl<br/><br/>Sample Web Application 2 with Hatena Web Service API : Hatena Keyword Quiz &amp; Visualization<br/><br/>by RyuK (klassphere[at.mark]gmail.com)<br/>http://zzz.zggg.com/<br/>http://aiueo.da.ru/<br/><br/>[Requirements (tested on Microsoft Windows XP)]<br/><br/>Mecab is compiled with ShiftJIS with the ShiftJIS dictionary.<br/>For platforms other than Windows, use UTF-8 for Mecab and its dic.<br/><br/>=cut<br/><br/>use 5.8.0;<br/><br/>use strict;<br/>use warnings;<br/><br/>use utf8;<br/>use Encode;<br/><br/>use YAML::Syck;<br/>use POE::Component::Server::HTTP;<br/>use HTTP::Status;<br/>use XML::RSS;<br/>use XMLRPC::Lite;<br/>use LWP::Simple;<br/>use MeCab;<br/>use URI::Escape;<br/><br/>$YAML::Syck::ImplicitTyping = 1;<br/><br/>my %quiz_answer = (); # IP address - answer<br/><br/>my $conf = YAML::Syck::LoadFile("hatenawebapp2.conf");<br/><br/></pre><br/><br/>ここでは、日本語UTF-8文字列を使うときにUTF-8に対応していない<code>XMLRPC::Lite</code>と<code>SOAP::Lite</code>内で問題がある箇所の関数を動的に上書き修正している。<br/><br/><pre class="brush: perl"><br/><br/># Patches some functions in XMLRPC::Lite and SOAP::Lite to pass a UTF-8 Japanese string<br/># in its HTTP transport that is a subclass of LWP::UserAgent<br/>$SOAP::Constants::DO_NOT_USE_LWP_LENGTH_HACK = 1;<br/><br/>{<br/> my $s =<<'SUBDOC';<br/>package XMLRPC::Serializer;<br/><br/>sub new<br/>{<br/> my $self = shift;<br/><br/> unless (ref $self)<br/> {<br/> my $class = ref($self) || $self;<br/> $self = $class->SUPER::new(<br/> typelookup =><br/> {<br/> base64 => [10, sub {1}, 'as_string'],<br/> int => [20, sub {$_[0] =~ /^[+-]?\d+$/}, 'as_int'],<br/> double => [30, sub {$_[0] =~ /^(-?(?:\d+(?:\.\d*)?|\.\d+)|([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?)$/}, 'as_double'],<br/> dateTime => [35, sub {$_[0] =~ /^\d{8}T\d\d:\d\d:\d\d$/}, 'as_dateTime'],<br/> string => [40, sub {1}, 'as_string'],<br/> },<br/> attr => {},<br/> namespaces => {},<br/> @_,<br/> );<br/> }<br/><br/> return $self;<br/>}<br/><br/>1;<br/><br/>package SOAP::Utils;<br/><br/>sub bytelength<br/>{<br/> return length($_[0]);<br/>}<br/><br/>1;<br/><br/>SUBDOC<br/><br/> no warnings;<br/> local $^W = 0;<br/> eval "$s";<br/> use warnings;<br/>}<br/><br/></pre><br/><br/>Webサーバの設定を行うとともに、Webサーバ上の各パス毎にハンドラ関数を登録している。<br/><br/><pre class="brush: perl"><br/><br/>my $aliases = POE::Component::Server::HTTP->new(<br/> Port => $conf->{bound_port},<br/> ContentHandler =><br/> {<br/> '/' => \&amp;handlerRoot,<br/> '/viz' => \&amp;handlerViz,<br/> '/quiz' => \&amp;handlerQuiz,<br/> '/answer' => \&amp;handlerAnswer,<br/> '/assoc' => \&amp;handlerAssoc,<br/> '/search' => \&amp;handlerSearch<br/> },<br/> Headers => { Server => 'My Server' },<br/>);<br/><br/></pre><br/><br/>これは穴埋めクイズの問題を作る関数で、要は、「はてなキーワード」内の日本語文に対しMeCabで形態素解析を行って、見つかった名詞の部分を隠すことによって穴埋め問題にするという至極単純な仕組みである。<br/><br/><pre class="brush: perl"><br/><br/>sub makeQuiz<br/>{<br/> my $s = shift;<br/> my $ip = shift;<br/><br/> my $sjis = ($^O =~ /mswin/i);<br/> if ($sjis)<br/> {<br/> utf8::encode($s);<br/> Encode::from_to($s, 'utf8', 'shiftjis');<br/> }<br/><br/> my $m = new MeCab::Tagger("");<br/> my $n = $m->parseToNode($s);<br/><br/> my @fragments = ();<br/><br/> my $count = 0;<br/> while ($n = $n->{next})<br/> {<br/> if (defined($n->{surface}))<br/> {<br/> my $w = $n->{surface};<br/> my $f = $n->{feature};<br/><br/> if ($sjis)<br/> {<br/> Encode::from_to($f, 'shiftjis', 'utf8');<br/> utf8::decode($f);<br/><br/> Encode::from_to($w, 'shiftjis', 'utf8');<br/> utf8::decode($w);<br/> }<br/><br/> if ($f =~ /^名詞/ &amp;&amp; length($w) >= 2)<br/> {<br/> push @fragments, [$w, 1];<br/> ++$count;<br/> }<br/> else<br/> {<br/> push @fragments, [$w, 0];<br/> }<br/> }<br/> }<br/><br/> if ($count == 0)<br/> {<br/> return "";<br/> }<br/><br/> my $masked_index = int(rand $count);<br/><br/> my $final = "";<br/> my $current_noun_index = 0;<br/> foreach my $f (@fragments)<br/> {<br/> if ($f->[1] == 0)<br/> {<br/> $final .= $f->[0];<br/> }<br/> elsif ($current_noun_index++ == $masked_index)<br/> {<br/> $final .= $conf->{quiz_mask};<br/><br/> if (keys(%quiz_answer) > 1000)<br/> {<br/> %quiz_answer = ();<br/> }<br/><br/> $quiz_answer{$ip} = $f->[0];<br/> }<br/> else<br/> {<br/> $final .= $f->[0];<br/> }<br/> }<br/><br/> return $final;<br/>}<br/></pre><br/><br/>webサーバのルートURLのハンドラ。ユーザが任意の単語を入力すると「はてなキーワード」を検索し、キーワード間の連想グラフをロードする。<br/><br/><pre class="brush: perl"><br/>sub handlerRoot<br/>{<br/> my ($request, $response) = @_;<br/> $response->code(RC_OK);<br/><br/> my $out = "";<br/> my $jsdir = $conf->{javascript_directory};<br/><br/> if ($request->uri =~ /$jsdir\/([^\/]+)$/)<br/> {<br/> my $f = $1;<br/> if (!open(FILE, "./$jsdir/$f"))<br/> {<br/> return RC_DENY;<br/> }<br/><br/> while (<FILE>)<br/> {<br/> $out .= $_;<br/> }<br/><br/> $response->content($out);<br/> $response->header('Content-type' => "application/x-javascript");<br/><br/> close(FILE);<br/><br/> return RC_OK;<br/> }<br/><br/> $out =<<'HEREDOC';<br/><html><br/><head><br/><title>Hatena web app 2</title><br/><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><br/><br/><script language="JavaScript"><br/><!--<br/><br/>function setup()<br/>{<br/><br/>}<br/><br/>function resizeIframe(f)<br/>{<br/> if (document.body.clientWidth)<br/> {<br/> f.style.width = document.body.clientWidth + "px";<br/> f.style.height = (document.body.clientHeight - 100) + "px";<br/> }<br/>}<br/><br/>function resizeIframe2(f)<br/>{<br/> if (document.body.clientWidth)<br/> {<br/> f.style.width = document.body.clientWidth + "px";<br/> }<br/>}<br/><br/>//--><br/><br/></script><br/><br/><style type="text/css"><br/><br/>body<br/>{<br/> margin: 0;<br/> padding: 0;<br/> overflow: hidden;<br/>}<br/><br/>p<br/>{<br/> text-decoration: none;<br/> font-size: 13px;<br/> font-weight: normal;<br/> font-family: Verdana, Geneva, san-serif;<br/> line-height: 150%;<br/> margin-top: 0px;<br/> margin-bottom: 1em;<br/> padding-left: 8px;<br/> padding-right: 8px;<br/>}<br/><br/>form<br/>{<br/> margin: 0;<br/> padding: 0;<br/>}<br/><br/></style><br/></head><br/><body onload="setup();" onresize="resizeIframe(document.getElementById('ifr'));resizeIframe2(document.getElementById('quiz'));"><br/><br><br/><p>Proof of Concept: Hatena Keyword Quiz &amp; Visualization</p><br/><br/><form onsubmit="frames.ifr.hatenaKeywords.getKeywords(this.word.value); return false;"><br/><p>はてなキーワード内から検索したい単語を <input type="text" id="word" size="40"> に入力し<br/><input type="submit" value="検索"> して、見つかった単語をクリックして下さい。<br/></p><br/></form><br/><p>(キーワードのノードはマウスでドラッグ可能です)</p><br/><iframe src="/quiz" id="quiz" name="quiz" width="1000" height="160" scrolling="yes" valign="top"<br/> onload="resizeIframe2(this);"></iframe><br/><iframe src="/viz" id="ifr" name="ifr" width="1000" height="600" scrolling="yes" valign="top"<br/> onload="resizeIframe(this);"></iframe><br/><br/></body><br/></html><br/><br/>HEREDOC<br/><br/> $response->content($out);<br/><br/> return RC_OK;<br/>}<br/><br/></pre><br/><br/>GETリクエストで呼び出されると必要なJavaScriptを表示し、POSTリクエストの場合は入力された単語を「はてなキーワード」で検索した後、キーワードのRSSデータから説明文を抜き出してクイズを作成表示する。<br/><br/><pre class="brush: perl"><br/><br/>sub handlerQuiz<br/>{<br/> my ($request, $response) = @_;<br/> $response->code(RC_OK);<br/><br/> my $out = "";<br/><br/> if ($request->method =~ /get/i)<br/> {<br/> $out =<<'HEREDOC';<br/><html><br/><head><br/><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><br/><br/><script language="JavaScript"><br/><!--<br/><br/>var isIE = window.ActiveXObject;<br/>var isMozilla = navigator.userAgent.indexOf('Gecko') != -1;<br/><br/>function createHttpRequest()<br/>{<br/> if (isIE)<br/> {<br/> try<br/> { // CLSID_XMLHTTP<br/> // v 3.0<br/> return new ActiveXObject("Msxml2.XMLHTTP");<br/> }<br/> catch (e)<br/> {<br/> try<br/> {// v 2.x<br/> return new ActiveXObject("Microsoft.XMLHTTP");<br/> }<br/> catch (e2)<br/> {<br/> return null;<br/> }<br/> }<br/> }<br/> else if (window.XMLHttpRequest) // non-IE<br/> {<br/> var hr = new XMLHttpRequest();<br/> if (isMozilla)<br/> hr.overrideMimeType('text/xml');<br/><br/> return hr;<br/> }<br/> else<br/> {<br/> return null;<br/> }<br/>}<br/><br/>function sendHTTP(data, method, uri, callback, async, caller)<br/>{<br/> var hr = createHttpRequest();<br/><br/> var args = new Array();<br/> args.push(hr);<br/> for (var i = 6; i < arguments.length; ++i)<br/> {<br/> args.push(arguments[i]);<br/> }<br/><br/> try<br/> {<br/> hr.open(method, uri, async);<br/> hr.setRequestHeader("If-Modified-Since", "Thu, 01 Jun 1970 00:00:00 GMT");<br/><br/> hr.onreadystatechange = function()<br/> {<br/> if (hr.readyState == 4)<br/> {<br/> callback.apply(caller, args);<br/> }<br/> }<br/><br/> hr.send(data);<br/> delete hr;<br/> }<br/> catch(e)<br/> {<br/> alert("sendHTTP: " + e);<br/> }<br/>}<br/><br/>function getTextContent(xml)<br/>{<br/> if (xml)<br/> {<br/> if (xml.textContent)<br/> return xml.textContent; // Mozilla<br/> // IE<br/> if (xml.innerText)<br/> return xml.innerText;<br/> if (xml.text)<br/> return xml.text;<br/> }<br/>}<br/><br/>function setup()<br/>{<br/><br/>}<br/><br/>function checkAnswer(answer)<br/>{<br/> sendHTTP(answer, "POST", "/answer", checkAnswerCallback, true, this, answer);<br/>}<br/><br/>function checkAnswerCallback(request)<br/>{<br/> var nodelist = request.responseXML.getElementsByTagName("a");<br/> if (nodelist &amp;&amp; nodelist.length != 0)<br/> {<br/> for (var i = 0; i < nodelist.length &amp;&amp; i < 10; ++i)<br/> {<br/> var e = nodelist.item(i);<br/> if (e.getAttribute("r") == "true")<br/> {<br/> alert("正解");<br/> }<br/> else<br/> {<br/> var answer = "";<br/> var n = e.firstChild;<br/> while (n != null)<br/> {<br/> // n.textContent == Mozilla only<br/> // NODE_TEXT == 3 || NODE_CDATA_SECTION == 4<br/> if (n != null &amp;&amp; (n.nodeType == 3 || n.nodeType == 4))<br/> {<br/> answer = n.nodeValue;<br/> }<br/><br/> n = n.nextSibling;<br/> }<br/><br/> alert("不正解 - 解答: " + answer);<br/> }<br/> }<br/> }<br/>}<br/><br/>//--><br/><br/></script><br/><br/><style type="text/css"><br/><br/>body<br/>{<br/> margin: 0;<br/> padding: 0;<br/> overflow: hidden;<br/>}<br/><br/>p<br/>{<br/> text-decoration: none;<br/> font-size: 13px;<br/> font-weight: normal;<br/> font-family: Verdana, Geneva, san-serif;<br/> line-height: 150%;<br/> margin-top: 0px;<br/> margin-bottom: 1em;<br/> padding-left: 8px;<br/> padding-right: 8px;<br/>}<br/><br/>form<br/>{<br/> margin: 0;<br/> padding: 0;<br/>}<br/><br/></style><br/></head><br/><body onload="setup();"><br/><span id="quiz"></span><br/></body><br/></html><br/><br/>HEREDOC<br/><br/> }<br/> else<br/> {<br/> $response->header('Content-Type' => 'text/xml');<br/> $out = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<r>\n";<br/><br/> my $w = $request->content;<br/> utf8::decode($w);<br/> $w =~ tr/A-Za-z0-9/A-Za-z0-9/;<br/> $w = URI::Escape::uri_escape_utf8($w);<br/><br/> my $content = get("http://d.hatena.ne.jp/keyword?word=$w&amp;mode=rss&amp;ie=utf8");<br/> if ($content)<br/> {<br/> my $rss = new XML::RSS;<br/><br/> utf8::decode($content);<br/><br/> $rss->parse($content);<br/><br/> my $item = ${$rss->{items}}[0];<br/><br/> for my $item (@{$rss->{items}})<br/> {<br/> $out .= "<i a='";<br/> $out .= $item->{link};<br/> $out .= "'>";<br/><br/> if (defined($item->{title}))<br/> {<br/> $out .= "<t><![CDATA[";<br/><br/> my $t = $item->{title};<br/> utf8::decode($t);<br/> $t =~ s/]]>/]]>/g;<br/> $out .= $t;<br/><br/> $out .= "]]></t>\n";<br/> }<br/><br/> if (defined($item->{description}))<br/> {<br/> $out .= "<d><![CDATA[";<br/><br/> my $d = $item->{description};<br/> utf8::decode($d);<br/> $d =~ s/]]>/]]>/g;<br/> $d =~ s|<a[^>]+>||g;<br/><br/> $out .= makeQuiz($d, $request->{connection}->{remote_ip});<br/><br/> $out .= "]]></d>";<br/> }<br/><br/> $out .= "</i>\n";<br/> }<br/> }<br/><br/> $out .= "</r>";<br/> }<br/><br/> $response->content($out);<br/><br/> return RC_OK;<br/>}<br/><br/></pre><br/><br/><a href="http://www.kylescholz.com/blog/projects/jsviz/" target="_blank">JSViz</a>によって「はてなキーワード」の連想グラフを視覚化した物を表示する画面のハンドラ。<br/><br/><pre class="brush: perl"><br/><br/>sub handlerViz<br/>{<br/> my ($request, $response) = @_;<br/> $response->code(RC_OK);<br/><br/> my $out = "";<br/> my $jsdir = $conf->{javascript_directory};<br/><br/> if ($request->uri =~ /$jsdir\/([^\/]+)$/)<br/> {<br/> my $f = $1;<br/> if (!open(FILE, "./$jsdir/$f"))<br/> {<br/> return RC_DENY;<br/> }<br/><br/> while (<FILE>)<br/> {<br/> $out .= $_;<br/> }<br/><br/> $response->content($out);<br/> $response->header('Content-type' => "application/x-javascript");<br/><br/> close(FILE);<br/><br/> return RC_OK;<br/> }<br/><br/> $out =<<'HEREDOC';<br/><html><br/><head><br/><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><br/><br/><script language="JavaScript" src="/jsdir/DataGraph.js"></script><br/><script language="JavaScript" src="/jsdir/Magnet.js"></script><br/><script language="JavaScript" src="/jsdir/Spring.js"></script><br/><script language="JavaScript" src="/jsdir/Particle.js"></script><br/><script language="JavaScript" src="/jsdir/ParticleModel.js"></script><br/><script language="JavaScript" src="/jsdir/Timer.js"></script><br/><script language="JavaScript" src="/jsdir/EventHandler.js"></script><br/><script language="JavaScript" src="/jsdir/HTMLGraphView.js"></script><br/><script language="JavaScript" src="/jsdir/SVGGraphView.js"></script><br/><script language="JavaScript" src="/jsdir/RungeKuttaIntegrator.js"></script><br/><script language="JavaScript" src="/jsdir/Control.js"></script><br/><script language="JavaScript" src="/jsdir/boxover.js"></script><br/><br/><script language="JavaScript"><br/><!--<br/><br/>var isIE = window.ActiveXObject;<br/>var isMozilla = navigator.userAgent.indexOf('Gecko') != -1;<br/><br/>// Suppress IE6 SP1 flicker<br/>if (isIE)<br/>{<br/> try<br/> {<br/> document.execCommand("BackgroundImageCache", false, true);<br/> }<br/> catch (e)<br/> {<br/><br/> }<br/>}<br/><br/>function createHttpRequest()<br/>{<br/> if (isIE)<br/> {<br/> try<br/> { // CLSID_XMLHTTP<br/> // v 3.0<br/> return new ActiveXObject("Msxml2.XMLHTTP");<br/> }<br/> catch (e)<br/> {<br/> try<br/> {// v 2.x<br/> return new ActiveXObject("Microsoft.XMLHTTP");<br/> }<br/> catch (e2)<br/> {<br/> return null;<br/> }<br/> }<br/> }<br/> else if (window.XMLHttpRequest) // non-IE<br/> {<br/> var hr = new XMLHttpRequest();<br/> if (isMozilla)<br/> hr.overrideMimeType('text/xml');<br/><br/> return hr;<br/> }<br/> else<br/> {<br/> return null;<br/> }<br/>}<br/><br/>function sendHTTP(data, method, uri, callback, async, caller)<br/>{<br/> var hr = createHttpRequest();<br/><br/> var args = new Array();<br/> args.push(hr);<br/> for (var i = 6; i < arguments.length; ++i)<br/> {<br/> args.push(arguments[i]);<br/> }<br/><br/> try<br/> {<br/> hr.open(method, uri, async);<br/> hr.setRequestHeader("If-Modified-Since", "Thu, 01 Jun 1970 00:00:00 GMT");<br/><br/> hr.onreadystatechange = function()<br/> {<br/> if (hr.readyState == 4)<br/> {<br/> callback.apply(caller, args);<br/> }<br/> }<br/><br/> hr.send(data);<br/> delete hr;<br/> }<br/> catch(e)<br/> {<br/> alert("sendHTTP: " + e);<br/> }<br/>}<br/><br/>function getTextContent(xml)<br/>{<br/> if (xml)<br/> {<br/> if (xml.textContent)<br/> return xml.textContent; // Mozilla<br/> // IE<br/> if (xml.innerText)<br/> return xml.innerText;<br/> if (xml.text)<br/> return xml.text;<br/> }<br/>}<br/><br/>var HatenaKeywords = function(dataGraph, particleModel)<br/>{<br/> this.init(dataGraph, particleModel);<br/>}<br/><br/>HatenaKeywords.prototype =<br/>{<br/> init: function(dataGraph, particleModel)<br/> {<br/> this.dataGraph = dataGraph;<br/> this.particleModel = particleModel;<br/><br/> this.TRAVERSE_DEPTH = 1;<br/> this.MAX_PRODUCTS_ORIGIN = 8;<br/> this.MAX_PRODUCTS_PER_SIMILARITY = 8;<br/> this.MAX_NODES = 20;<br/><br/> this.nodesByName = {};<br/> this.nodesCount = 0;<br/> },<br/><br/> search: function(keyword)<br/> {<br/> document.getElementById('searchResults').innerHTML = "";<br/> document.getElementById('keywordResults').style.display = "none";<br/><br/> parent.document.getElementById('word').value = keyword;<br/><br/> this.particleModel.clear();<br/> this.nodesByName = {};<br/> this.nodesCount = 0;<br/><br/> if (this.particleModel.timer.interupt)<br/> this.particleModel.timer.start();<br/><br/> var node = new DataGraphNode(true, 2);<br/> node.keyword = keyword;<br/><br/> this.dataGraph.addNode(node);<br/> this.nodesByName[keyword] = node;<br/> this.getSimilarKeywords(keyword, 0);<br/><br/> sendHTTP(keyword, "POST", "/quiz", this.getQuizCallback, true, this, keyword);<br/> },<br/><br/> getQuizCallback : function(request)<br/> {<br/> var nodelist = request.responseXML.getElementsByTagName("i");<br/> if (nodelist &amp;&amp; nodelist.length != 0)<br/> {<br/> for (var i = 0; i < nodelist.length &amp;&amp; i < 10; ++i)<br/> {<br/> var e = nodelist.item(i);<br/> var keyword = getTextContent(e.getElementsByTagName("t")[0]);<br/> if (!keyword)<br/> keyword = "";<br/><br/> var desc = getTextContent(e.getElementsByTagName("d")[0]);<br/> if (!desc)<br/> desc = "";<br/><br/> parent.frames.quiz.document.getElementById('quiz').innerHTML<br/> = "<p>クイズ: 隠された単語は何でしょう?</p><p>"<br/> + desc<br/> + "</p><form onsubmit=\"checkAnswer(this.answer.value);return false;\"><p>"<br/> + "あなたの答え: <input id=\"answer\" type=\"text\" size=\"20\"><input type=\"submit\""<br/> + " value=\"解答をチェック\"></p></form>";<br/> }<br/> }<br/> },<br/><br/> getKeywords : function(keyword)<br/> {<br/> document.getElementById('searchResults').innerHTML = "<p><blink>Searching...</blink></p>";<br/><br/> this.particleModel.clear();<br/> this.nodesByName = {};<br/> this.nodesCount = 0;<br/><br/> if (this.particleModel.timer.interupt)<br/> this.particleModel.timer.start();<br/><br/> sendHTTP(keyword, "POST", "/search", this.getKeywordsCallback, true, this, keyword);<br/> },<br/><br/> getKeywordsCallback : function(request)<br/> {<br/> document.getElementById('searchResults').innerHTML = "";<br/><br/> var nodelist = request.responseXML.getElementsByTagName("i");<br/> if (nodelist &amp;&amp; nodelist.length != 0)<br/> {<br/> var h = document.createElement('p');<br/> h.innerHTML = (nodelist.length.toString() + "個のキーワードが見つかりました。クリックすると関係する単語群を探せます。");<br/> document.getElementById('searchResults').appendChild(h);<br/><br/> for (var i = 0; i < nodelist.length &amp;&amp; i < 10; ++i)<br/> {<br/> var e = nodelist.item(i);<br/> var keyword = getTextContent(e.getElementsByTagName("t")[0]);<br/> if (!keyword)<br/> keyword = "";<br/><br/> var desc = getTextContent(e.getElementsByTagName("d")[0]);<br/> if (!desc)<br/> desc = "";<br/><br/> var r = document.createElement('div');<br/> r.className = "keyword";<br/> var title = ("fade=[on] header=[" + keyword.replace(/[/g, "").replace(/]/g, "")<br/> + "] body=[" + desc.replace(/[/g, "").replace(/]/g, "") + "]");<br/> r.setAttribute("title", title);<br/> r.setAttribute("style", "padding-left: 50px;");<br/> r.innerHTML = '<p onclick="' +<br/> "hatenaKeywords.search('" + keyword.replace(/"/g, '"') + "')" + '">'<br/> + "<b><a onmouseover=\"this.style.textDecoration = 'underline'\" onmouseout=\"this.style.textDecoration = 'none'\">"<br/> + keyword + '</b></a></p>';<br/><br/> document.getElementById('searchResults').appendChild(r);<br/><br/> }<br/> }<br/> },<br/><br/> getSimilarKeywords: function(word, ordinal)<br/> {<br/> sendHTTP(word, "POST", "/assoc", this.getSimilarKeywordsCallback, true, this, word, ordinal);<br/> },<br/><br/> getSimilarKeywordsCallback: function(request, parentWord, ordinal)<br/> {<br/> var max = this.MAX_PRODUCTS_PER_SIMILARITY;<br/> if (ordinal == 0)<br/> max = this.MAX_PRODUCTS_ORIGIN;<br/><br/> var nodelist = request.responseXML.getElementsByTagName("related");<br/> for (var i = 0; i < nodelist.length &amp;&amp; i < max &amp;&amp; this.nodesCount < this.MAX_NODES; i++)<br/> {<br/> var word = nodelist[i].getAttribute("w");<br/><br/> if (this.nodesByName[word])<br/> {<br/> var node = this.nodesByName[word];<br/> this.dataGraph.addEdge(node, this.nodesByName[parentWord]);<br/> }<br/> else<br/> {<br/> var node = new DataGraphNode(false, 1);<br/> node.keyword = word;<br/><br/> node.addEdge(this.nodesByName[parentWord], 1);<br/> this.dataGraph.addNode(node);<br/> this.nodesCount++;<br/><br/> this.nodesByName[word] = node;<br/><br/> if (ordinal < this.TRAVERSE_DEPTH)<br/> this.getSimilarKeywords(word, ordinal + 1);<br/> }<br/> }<br/> }<br/>}<br/><br/>var hatenaKeywords;<br/><br/>function setup()<br/>{<br/> var FRAME_WIDTH;<br/> var FRAME_HEIGHT;<br/><br/> if (document.all)<br/> {<br/> FRAME_WIDTH = document.body.offsetWidth - 10;<br/> FRAME_HEIGHT = document.documentElement.offsetHeight - 10 - 28;<br/> }<br/> else<br/> {<br/> FRAME_WIDTH = window.innerWidth - 10;<br/> FRAME_HEIGHT = window.innerHeight - 10 - 28;<br/> }<br/><br/> var view = document.implementation.hasFeature("org.w3c.dom.svg", '1.1') ?<br/> new SVGGraphView(0, 26, FRAME_WIDTH, FRAME_HEIGHT, true)<br/> : new HTMLGraphView(0, 26, FRAME_WIDTH, FRAME_HEIGHT, true);<br/><br/> var particleModel = new ParticleModel(view);<br/> particleModel.start();<br/><br/> var control = new Control(particleModel, view);<br/> var dataGraph = new DataGraph();<br/><br/> hatenaKeywords = new HatenaKeywords(dataGraph, particleModel);<br/><br/> var nodeHandler = new NodeHandler(dataGraph, particleModel, view, control);<br/> dataGraph.subscribe(nodeHandler);<br/><br/> var buildTimer = new Timer(150);<br/> buildTimer.subscribe(nodeHandler);<br/> buildTimer.start();<br/>}<br/><br/>var NodeHandler = function( dataGraph, particleModel, view, control )<br/>{<br/> this.dataGraph = dataGraph;<br/> this.particleModel = particleModel;<br/> this.view = view;<br/><br/> this.nodeQueue = new Array();<br/> this.relationshipQueue = new Array();<br/><br/> this['newDataGraphNode'] = function(dataGraphNode)<br/> {<br/> this.enqueueNode(dataGraphNode);<br/> }<br/><br/> this['newDataGraphEdge'] = function(nodeA, nodeB)<br/> {<br/> this.enqueueRelationship(nodeA, nodeB);<br/> }<br/><br/> this['enqueueNode'] = function(dataGraphNode)<br/> {<br/> this.nodeQueue.push(dataGraphNode);<br/> }<br/><br/> this['enqueueRelationship'] = function(nodeA, nodeB)<br/> {<br/> this.relationshipQueue.push({'nodeA': nodeA, 'nodeB': nodeB});<br/> }<br/><br/> this['dequeueNode'] = function()<br/> {<br/> var node = this.nodeQueue.shift();<br/><br/> if (node)<br/> {<br/> this.addParticle(node);<br/> return true;<br/> }<br/><br/> return false;<br/> }<br/><br/> this['dequeueRelationship'] = function()<br/> {<br/> var edge = this.relationshipQueue.shift();<br/> if (edge)<br/> this.addSimilarity(.05, edge.nodeA, edge.nodeB);<br/> }<br/><br/> this.update = function()<br/> {<br/> var nodes = this.dequeueNode();<br/> if (!nodes)<br/> this.dequeueRelationship();<br/> }<br/><br/> this['addParticle'] = function(dataGraphNode)<br/> {<br/> particle = this.particleModel.makeParticle(dataGraphNode.mass, 0, 0);<br/> dataGraphNode.particle = particle;<br/><br/> if (dataGraphNode.fixed)<br/> particle.fixed = true;<br/><br/> var rx = Math.random() * 2 - 1;<br/> var ry = Math.random() * 2 - 1;<br/> particle.positionX = rx - 50 / this.view.skew;<br/> particle.positionY = ry;<br/><br/> for (var j = 0, l = this.particleModel.particles.length; j < l; j++)<br/> {<br/> if (this.particleModel.particles[j] != particle)<br/> this.particleModel.makeMagnet(particle, this.particleModel.particles[j], -30000, 64);<br/> }<br/><br/> var particleParent = false;<br/><br/> for (var c in dataGraphNode.edges)<br/> {<br/> if (!particleParent)<br/> {<br/> particleParent = true;<br/> particle.positionX = dataGraphNode.edges[c].particle.positionX + rx;<br/> particle.positionY = dataGraphNode.edges[c].particle.positionY + ry;<br/> }<br/><br/> this.addSimilarity(.2, dataGraphNode, dataGraphNode.edges[c]);<br/> }<br/><br/> var keyword = dataGraphNode.keyword;<br/> if (!keyword) {keyword = "";}<br/><br/> var n = document.createElement('div');<br/> n.style.position = "absolute";<br/> n.className = "keyword";<br/> n.innerHTML = '<div onclick="'<br/> + "hatenaKeywords.search('" + keyword.replace(/"/g,'"') + "')"<br/> + "\"><a onmouseover=\"this.style.textDecoration = 'underline'\" onmouseout=\"this.style.textDecoration = 'none'\">"<br/> + keyword + '</a></div>';<br/> n.onmousedown = new EventHandler(control, control.handleMouseDownEvent, particle.id)<br/> dataGraphNode.viewNode = this.view.addNode(particle, n, 25, isIE ? 10 : 30);<br/><br/> //particle.width = dataGraphNode.viewNode.offsetWidth;<br/> //particle.height = dataGraphNode.viewNode.offsetHeight;<br/><br/> return dataGraphNode;<br/> },<br/><br/> this['addSimilarity'] = function(springConstant, nodeA, nodeB)<br/> {<br/> particleModel.makeSpring(nodeA.particle, nodeB.particle, springConstant, .2, 80);<br/><br/> var props = document.implementation.hasFeature("org.w3c.dom.svg", '1.1') ?<br/> {<br/> 'stroke': "#bbbbbb",<br/> 'stroke-width': '2px',<br/> 'stroke-dasharray': '2, 8'<br/> }<br/> :<br/> {<br/> 'pixelColor': "#aaaaaa",<br/> 'pixelWidth': '2px',<br/> 'pixelHeight': '2px',<br/> 'pixels': 15<br/> };<br/><br/> this.view.addEdge(nodeA.particle, nodeB.particle, props);<br/> }<br/>}<br/><br/>//--><br/><br/></script><br/><br/><style type="text/css"><br/><br/>body<br/>{<br/> margin: 0;<br/> padding: 0;<br/> overflow: hidden;<br/>}<br/><br/>p<br/>{<br/> text-decoration: none;<br/> font-size: 13px;<br/> font-weight: normal;<br/> font-family: Verdana, Geneva, san-serif;<br/> line-height: 150%;<br/> margin-top: 0px;<br/> margin-bottom: 1em;<br/> padding-left: 8px;<br/> padding-right: 8px;<br/>}<br/><br/>form<br/>{<br/> margin: 0;<br/> padding: 0;<br/>}<br/><br/>div.keyword<br/>{<br/> font-family: Verdana, Geneva, san-serif;<br/> font-weight: bold;<br/> font-size: 14px;<br/> text-align: left;<br/> background-repeat: no-repeat;<br/> cursor: hand;<br/>}<br/><br/>#keywordResults<br/>{<br/> display: none;<br/> color: #000000;<br/> border-top: 0px;<br/>}<br/><br/></style><br/></head><br/><br/><body onload="setup();"><br/><div id="searchResults"></div><br/><div id="keywordResults"></div><br/></body><br/></html><br/><br/>HEREDOC<br/><br/> $response->content($out);<br/><br/> return RC_OK;<br/>}<br/><br/></pre><br/><br/>「はてなキーワード」で検索を行うためのAJAXコールバックを定義する。<a href="http://www.kylescholz.com/blog/projects/jsviz/" target="_blank">JSViz</a>はこれを呼び出して見つかった関連単語を次々と検索し、単語のグラフに新しいノードを付け加えていく。<br/><br/><pre class="brush: perl"><br/><br/>sub handlerSearch<br/>{<br/> my ($request, $response) = @_;<br/> $response->code(RC_OK);<br/><br/> if ($request->method =~ /get/i)<br/> {<br/> return RC_DENY;<br/> }<br/><br/> $response->header('Content-Type' => 'text/xml');<br/> my $out = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<r>\n";<br/><br/> my $w = $request->content;<br/> utf8::decode($w);<br/> $w =~ tr/A-Za-z0-9/A-Za-z0-9/;<br/> $w = URI::Escape::uri_escape_utf8($w);<br/><br/> my $content = get("http://search.hatena.ne.jp/keyword?word=$w&amp;mode=rss&amp;ie=utf8&amp;page=1");<br/> if ($content)<br/> {<br/> my $rss = new XML::RSS;<br/><br/> utf8::decode($content);<br/><br/> $rss->parse($content);<br/><br/> for my $item (@{$rss->{items}})<br/> {<br/> $out .= "<i a='";<br/> $out .= $item->{link};<br/> $out .= "'>";<br/><br/> if (defined($item->{title}))<br/> {<br/> $out .= "<t><![CDATA[";<br/><br/> my $t = $item->{title};<br/> utf8::decode($t);<br/> $t =~ s/]]>/]]>/g;<br/> $out .= $t;<br/><br/> $out .= "]]></t>\n";<br/> }<br/><br/> if (defined($item->{description}))<br/> {<br/> $out .= "<d><![CDATA[";<br/><br/> my $d = $item->{description};<br/> utf8::decode($d);<br/> $d =~ s/]]>/]]>/g;<br/> $out .= $d;<br/><br/> $out .= "]]></d>";<br/> }<br/><br/> $out .= "</i>\n";<br/> }<br/> }<br/><br/> $out .= "</r>";<br/><br/> $response->content($out);<br/><br/> return RC_OK;<br/>}<br/><br/></pre><br/><br/>関連単語を「はてな」の関連キーワードAPIを用いて探すためのハンドラ。「はてな」のコードサンプルで推奨されているようにXMLRPC::Liteモ ジュールを使って検索するが、私が試した限りでは「ディスク」など一部単語で問題が起こるようで、いささか実用性に欠ける。<br/><br/><pre class="brush: perl"><br/><br/>sub handlerAssoc<br/>{<br/> my ($request, $response) = @_;<br/> $response->code(RC_OK);<br/><br/> if ($request->method =~ /get/i)<br/> {<br/> return RC_DENY;<br/> }<br/><br/> $response->header('Content-Type' => 'text/xml');<br/> my $out = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<r>\n";<br/><br/> my $w = $request->content;<br/> utf8::decode($w);<br/> $w =~ tr/A-Za-z0-9/A-Za-z0-9/;<br/> utf8::encode($w);<br/><br/> # XML::Parser employed by XMLRPC::Lite has troubles for some words such as "ディスク".<br/> # Probably should avoid XMLRPC::Lite and use another module instead in future.<br/> eval<br/> {<br/> my $res = XMLRPC::Lite->new->proxy('http://d.hatena.ne.jp/xmlrpc')->call(<br/> 'hatena.getSimilarWord', {wordlist => [$w]}<br/> );<br/><br/> unless ($res->fault)<br/> {<br/> foreach (@{$res->result->{wordlist}})<br/> {<br/> if (defined($_->{word}))<br/> {<br/> $out .= "<related w='";<br/> $out .= $_->{word};<br/> $out .= "'/>\n";<br/> }<br/> }<br/> }<br/> };<br/> warn $@ if $@;<br/><br/> $out .= "</r>";<br/><br/> $response->content($out);<br/><br/> return RC_OK;<br/>}<br/><br/></pre><br/><br/>クイズの答えが正しいかどうか判定し、正解/不正解を返すAJAXコールバックURLのハンドラ。<br/><br/><pre class="brush: perl"><br/><br/>sub handlerAnswer<br/>{<br/> my ($request, $response) = @_;<br/> $response->code(RC_OK);<br/><br/> if ($request->method =~ /get/i)<br/> {<br/> return RC_DENY;<br/> }<br/><br/> $response->header('Content-Type' => 'text/xml');<br/> my $out = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<r>\n";<br/><br/> if (exists($quiz_answer{$request->{connection}->{remote_ip}}))<br/> {<br/> my $user_input = $request->content;<br/> utf8::decode($user_input);<br/><br/> if ($quiz_answer{$request->{connection}->{remote_ip}} eq $user_input)<br/> {<br/> $out .= "<a r=\"true\"/>";<br/> }<br/> else<br/> {<br/> my $answer = $quiz_answer{$request->{connection}->{remote_ip}};<br/> $answer =~ s/]]//g;<br/> $out .= "<a r=\"false\"><![CDATA[" . $answer . "]]></a>";<br/> }<br/> }<br/><br/> $out .= "</r>";<br/><br/> $response->content($out);<br/><br/> return RC_OK;<br/>}<br/><br/>POE::Kernel->run;<br/>exit;<br/></pre><br/><br/>最後に<code>POE::Kernel</code>を呼び出し、サーバを起動する。<br/><br/>2つを書いてみての感想は、モジュール周りの扱いがRubyの方が簡潔で、完成された印象を持った。Perlの方はかなり入り組んでいてモジュールのインストールだけで小一時間かかってしまう(ほとんどがPOEに起因しているが、XML関連モジュールの層も相当ファットである印象を受ける)。Perlの方だけUTF-8を扱う必要がある点も、各モジュールの問題が噴出し、Perlにとって不利な結果となった。もちろんRubyも Mongrelの完成度が低いといった問題はあるものの、Webアプリケーションそのものの問題ではないし、それを言えばPOEは完全にMongrelに劣るので、Webアプリケーションテスト環境を含めた評価としては、やはりRubyの方が洗練されている。今回は順当にRubyに軍配が上がる結果となった。これでRubyそのもののパフォーマンスが向上すれば鬼に金棒といえるだろう。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-86587794994088357102006-12-12T16:57:00.000+09:002015-02-26T02:27:32.917+09:00Minuano2006年は更新1回などと前回書きながらまたこのページを更新する。今回はつまらない作業のメモ。<br />
<br />
<a href="http://mongrel.rubyforge.org/" target="_blank">Mongrel</a>というRubyのモジュールがあって、これはWebサーバの機能を持っている。似たようなモジュールとして<a href="http://www.webrick.org/" target="_blank">WEBrick</a>というのがあるけれども<a href="http://mongrel.rubyforge.org/" target="_blank">Mongrel</a>の方が新しくまたパフォーマンスも高いらしいというので昨日拾って中身を見てみるとmongrel(雑種)という名前に何か関係があるのかHTTPパーザの部分だけC拡張として実装してある。ソケット周りの部分をもCにしない理由は不明だが、大量のデータを処理するホットスポットをCにするという方針自体は正しい。<br />
<a name='more'></a>だが、こうしたCを使っているモジュールの常か、クロスプラットフォーム対応はおざなりなようでWin32版のバイナリの提供が若干遅 れている。とあるテストをWin32版で行っていて不具合らしき現象に突き当たったので自分で新しい物をビルドしようと思いRubyForgeのSVNから未リリースのバージョンを取ってきた。どうやらメジャーバージョンアップを控えているようで色々中身が増えている。C拡張ソースコードがある<code>ext\http11</code>フォルダの<code>extconf.rb</code>が<code>mkmf</code>ライブラリによるMakefile生成スクリプトなのでこれをRubyで実行する。(ちなみにRuby本体は<a href="http://www.garbagecollect.jp/ruby/mswin32/ja/">Ruby-mswin32</a> のビルドを使用している。) ところがエラーが出てMakefileを作ってくれない。<code>mkmf.log</code>を見ると<code>c.lib</code>と いうのがおかしいようだ。とどのつまりUnixのCライブラリが前提になっていてWindowsでのビルドは全く無視されていると思われる。ドキュメントはビルドについて何も触れていないので、オフィシャルのWin32版がどうやってビルドされたのかすらわからない。<br />
<br />
仕方ないので試行錯誤して<code>extconf.rb</code>内の<code>have_library</code>の第一引数を<code>libcpmt</code>(VC++のマルチスレッドリリース版Cライブラリ)にしてやるとコンフリクトも無くどうにかMakefile を作ってくれる。nmakeによるビルドの際はRuby本体の<code>lib\ruby\1.8\i386-mswin32</code>フォルダにある<code>config.h</code> 先頭のコンパイラバージョンチェックにひっかかるので、これを編集してコメントアウトする必要がある。Makefile自体もまだ不完全で、ライブラリがエクスポートする関数を示すdefファイルをRubyで生成する手順になっているのが、Rubyインタプリタへのパスに空白と スラッシュが含まれているためにうまく動作せず、そのまま実行するとライブラリのバイナリ(<code>http11.so</code>)はできるものの何も関数が入っていないという状態になってしまう。そこで二重引用符でRubyインタプリタのパスを囲みスラッシュを\に変えなければならない。これでようやくまともな<code>http11.so</code>が出来上がるのでこれを<a href="http://mongrel.rubyforge.org/" target="_blank">Mongrel</a>のlibフォルダへ移動する。<br />
<br />
最新版でチェックしても問題は解決しなかった。その問題とは、Firefoxではページが表示されるのにIE7だととにかく「ページを 表示できない」というDNSエラーの表示が出てどうにもならないというものである。タイムアウトはしていないのでソケット接続はしているようである。ま た、Windowsファイアウォールも適切に設定してあり、それ以前にファイアウォールの内側でテストしているのだから関係があるはずもない。冒頭に述べ たように幸いソケット部分はRubyなので簡単にデバッグできそうかな等と思い始めた頃にはたと思い当たり<code>Mongrel::Configurator</code>で指定しているリクエストチェインから<code>Mongrel::DeflateFilter</code>を抜いてみる。これでやっとIE7でページを開くことが出来た。どうやらFirefoxと異なり圧縮モードへのIE7の対応に問題があったらしい。このライブラリが金を払う必要のある製品でないことだけが不幸中の幸いである。だが失った時間は帰ってこない。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-81357892770010308462006-11-30T16:56:00.000+09:002015-02-26T02:28:04.770+09:00Shonuff去年は1回しかこのサイトを更新できなかったと述べているうちにすでに2006年も末で今年もこのDICE v0.86リリースに合わせての年1回の更新となる。書きためていた文章もいくつかアップロードした。サイトの全体的な改装なども行いたいけれどなかなか難しい。リンク切れの修正程度が精一杯だ。<br />
<br />
Webサイトを公開し始めて7年、このサーバに移ってきてから4年経つ。DICEも4年が経ち、かなり大規模なアプリケーションになった。4種類のサーバが1つになっている上にGUIクライアントやWebアプリケーションのスクリプトなどが入って、一人でメインテナンスするには正直言ってかなり厳しいサイズになっている。ドキュメント類の更新だけでも一仕事だ。アプリケーションのライセンスであるとか、ソフトウェアにまつわる形式的な物事に対して自分で決定し細部まで凝るのが以前は楽しみだった。<br />
<br />
プログラミングそのものについても一貫していて、DICEのデザインについてみると、自分がかつて書いた<a href="http://zzz.zggg.com/category/code/dice/">2本の記録</a>を読み返してみても構築的な欲求や形式へのこだわりを強く感じる。ネットワークセキュリティにも強い興味があり、DICEの設計も偏執狂的にセキュリティを追及していた。IRCサーバというのは、他の種類のサーバに比べ、一つの独立した世界を創造するという側面が非常に強い。OSからハードウェアの抽象化という機能を抜いたような物だと考えれば案外近いかも知れない。セキュリティは、作り出した世界を確固としたものとするためには必須要件である。<br />
<br />
また、DICEを作り始めた2001年頃には、デザインパターンやeXtreme Programming、RUP、アスペクト指向などの、ポストOOとでもいうべきソフトウェア工学のトレンドが旺盛に議論されていて、その頃は私もその関係の本やドキュメントを一生懸命追いかけていた。当時の自分のスタンスはどちらかというと保守的なもので、eXtreme Programmingのような教えは米国のソフトウェアコンサルタントの新しい飯の種にすぎない、自己啓発セミナーのようで胡散臭い、といった見方だった。また、色々なプログラミング言語を勉強したり、積極的に新しいプログラミングのトレンドを追いかけたりもした。C++のテンプレートなどもその一つで、自分の<a href="http://zzz.zggg.com/2003/08/16/template-metaprogramming-for-dummies/">3年前の文章</a>を見るに、当時はかなり興味を覚えていたようだ。余所余所しい書き方をするのは、関心が他に移っていったからに他ならない。その徴候はこのサイトの<a href="http://zzz.zggg.com/2004/04/28/dice-version-0-84/" target="_blank">2004年4月28日の項</a>に現れている。<br />
<a name='more'></a><br />
実は、DICEを作りつつも、3年以上前から別のネットワークアプリケーションを作ろうと考え色々と準備してきた。DICEにはTCP/IP ネットワークアプリケーションの全てが入っている。ファイルの送受信、通信の暗号化、多数のクライアントセッションのホスト、クライアントセッションへのグループ(チャンネル)毎へのメッセージブロードキャスト、HTTPによるファイアウォール越え、データ レコードの検索、データベースとの接続、.NETコンポーネントのホスト、GUIクライアント、Web GUI等、要はDICEというのは私にとってのWindowsネットワークアプリケーションC++ライブラリのサンプルアプリケーションだ。DICEの中に入っている物を組み替えればどんなTCP/IPアプリケーションでも高パフォーマンスを維持しつつ作れる。ツールは全て揃っている。アマチュアゲーム製作の際にやってはいけないことの筆頭として「自分用のライブラリを製作すること」というのがあるそうで、それは「いつまで経っても目的のゲームが完成しないから」だそうだ。DICEの場合、作り始めて4年で、自分用のライブラリ、ツールキットとしてはほとんど完成したのではないかというところまで、やっと到達できた。<br />
<br />
新しいアプリケーションはもちろんDICEの部品を積極的に活用する。今度はクライアントとしての側面を強調したアプリケーションにな る予定だ。開発スタイルも、DICEのようにドキュメントも含めて完成したパッケージとするのではなく、もっとカジュアルでインクリメンタルなものにしたい。それこそbloggerがblogを毎日記すように、頻繁に少しずつ更新したプログラムをアップロードしたい。セットアップもzipアーカイブの中身を取り出すだけの簡単な物が良いかもしれない。DICEでは一旦更新するとなるとドキュメントやパッケージの更新も必要で大がかりになり、アプリケーションそのもののスピーディな更新ができなかった。気がついたら、自分が軽んじていたアジャイルソフトウェア開発のようなスタイルに惹かれていたのは何とも皮肉だ。こだわりやプライドを捨てることのなんと大変なことか。歳を取ってわかることはあると実感する。ツールの研究は十分やった。だから、こだわりを捨てて謙虚に、より多くの人に必要とされるようなソフトウェアを書きたい。応用(application)というのがキーワードだ。<br />
<br />
しかしながら、より良いツールに対する興味も完全に失われたわけではなくて、どうやればクライアント向けの使いやすいユーザインター フェイスを作れるかについて、もうかなりの時間悩んでいる。最初に設定したユーザインターフェイスの要件は、1. ユーザが自由に改変できるものであること 2. 短期間で要素技術が陳腐化しないこと である。この要件に合致するには何を選べばよいか、悩み続けている。ユーザによる改変可能性については、ユーザインターフェイスはユーザが常に触れる物なのでユーザが好き勝手に改変できるのが最良だろうという価値判断による。改変可能性の程度としては、スキン変更といった見栄えのみの変更に留まらず、ユーザとアプリ ケーションコア間でのインターフェイスとしてのデータストリームを全てユーザが制御できるようにしたい。ある意味Unixシェルのリダイレクトやパイプの概念と同じである。プログラム全体をオープンソースにすれば原理的にこの問題は解決するが、それをせずに、アプリケーションの一定部分のみプログラマブル にしたいというわけだ。パフォーマンス上の観点からも、パフォーマンスの要る部分はネイティブコンパイル済みで、それ以外はユーザが自由に変更できるという構成は理にかなっている。何よりそんなアプリケーションがあったら私自身が欲しい。<br />
<br />
DICEを最初にリリースしたときから管理用GUIクライアントとして添付しているDICEAdminShellはMFCで作られているC++アプリケーションである。当然ユーザによる改変は出来ないし、MFCというライブラリは今ではほとんど進歩の望めない陳腐化したテクノロジになっ てしまっている。これではいかにも面白くない。そこで、次に候補になるのは.NETだ。2004年にはDICEが.NETコンポーネントを ホストできるようにした。これはWebサーバ向けの機能として入れた物でWebアプリケーション実行が主用途だが、GUIを持ちユーザの デスクトップとインタラクトするアプリケーションも実行できる。現在のMS SQLサーバはストアドプロシージャをC#などで記述でき、同じようにDICEもCLRをホストしている。CodeDomを用いてC#などのソースコードをコンパイルしてアセンブリにした上でロードし実行する機能も入れてあるので、ユーザによるコード改変も容易である。問題は、GUIのコードを Windows Formsで開発するのが良いことなのかということだ。あと1年もすればWPF/XAMLがWindowsアプリケーションの標準ユーザインターフェイス定義言語になることは目に見えている。もう一つの可能性として、C#やC++/COMでIEコンポーネントを使う、あるいはGecko/XULでアプリケーションを構築するという道がある。IEコンポーネントの方は私自身が日常的にIEを使っていない関係上IEウィジェットの見栄えや挙動に縛られるのは個人的に嫌だし、Mozillaの方はお世辞にも開発サポートが豊富とは言い難くまたランタイムの配布サイズも大きくなってしまいそうだ。<br />
<br />
そこで、最後に検討しているのはWeb UIである。DICEにはIEとFirefoxの双方で使える<a href="http://zzz.zggg.com/2006/11/12/server-push-today-marginal-ajax/">Web IRCクライアント</a>を作って入れた。これはJavaScriptでタブ切り替えのUIを構成し、AJAXのHTTPリクエストではなくFlashでサーバ本体との通信を行っている。画面遷移も勿論無い。DICEにはWebサーバ機能があるので、リモート用途でなくともWeb UIを簡単に付けられるというわけだ。もちろん、通信を暗号化すればインターネットを経由したリモートアクセスのためのUIと しても使える。Web UIの短所は、右クリックコンテクストメニューがフックできない場合があること、クリップボードやサウンド再生が使えないことである。クリップボードやサウンド再生は、リモートからアクセスするのではなくローカルで動かす場合なら本体の方で処理すればよいのでそれなりに対処できるが、右クリックコンテクストメニューはブラウザ自身のメニューとの兼ね合いで問題がある。その点のみ無視すれば、基本的に、フレームバッファの内容に直接演算を適用しないと得られない特殊な視覚効果を除き、普通のデスクトップアプリケーション並みのリッチなUIをWeb UIでも実現できる。残る課題はFlashとサーバとの通信の暗号化くらいだけれども、それも目処が付いている。<br />
<br />
そういうわけで、今考えているデザインは、UIはWebブラウザを使って、それでカバーできない部分は上記の.NET CLRホスティングでC#ソースコードを公開しユーザが改変できるようにしたいと思っている。アプリケーションのコアは今まで 通りC++で、パフォーマンスの要らない部分は可能な限り.NETの方へ回す。このページの下にある私が書いた2004年の文章では 「C++が格下の存在になりつつある」などと書いているが、実態は全然そんなことはなかったようで、Windows Vistaですら.NETは一定の範囲に留められている。DICEに<a href="http://zzz.zggg.com/2006/11/15/perl-ruby-multithreading-embedding/">Perlも組み 込んだ</a>し、要は臨機応変というわけだ。フットワークの軽さなら自信がある。<br />
<br />
以上が今後公開しようと思っているアプリケーションの話。このサイトで他にやっているのはプログラミング関係の雑文の公開だが、一番反響があるのは意外なことに<a href="http://zzz.zggg.com/2005/05/05/how-to-programmatically-create-self-signed-cert-key-pair-windows-sspi/">SSLの証明書を証明書ストアに入れる方法の記事</a>だった。資料が無くて困っているのは皆同じのようである。開発者が感謝のメールをくれたりする。中には記事の範囲を超えてあれこれと相談し てくる人もいた。自分でビジネスをやっている人間は自分で問題解決して欲しいところだ。とは言っても私もFirefoxのBugzillaではオープンソースプロジェクトなのを良いことにかなり好き勝手な注文を付けたりしているので人のことは言えない。他人のリポートしたバグのプロパティを変更する権限を私は持っているので、他人のバグには介入するわで余計始末が悪い。<br />
<br />
最近は諸々の理由で全くPCゲームをやっていない。一方で、ゲーム機のハードウェアへの興味が約2年前から非常に高まった。メインストリームPCがようやく並列プログラミングに入ろうかという時期に、ゲーム機は既に3コアとか9コアといった並列性の高いアーキテクチャをCPUに採用している。GPUの並列性はさらに高い。また、パフォーマンスがとかく軽視されがちなWebなどのプログラミングと異なり、パフォーマンスが何時の時代も追及されるのはゲームの分野である。ネットワークプログラミングも、MMOGやオンラインFPSなど一つのワールドイメージを維持しようとした場合の負荷は非常に高い。<br />
<br />
というわけでここ2年は<a href="http://forum.beyond3d.com/forumdisplay.php?f=37" target="_blank">Beyond3DのConsole Forum</a>に入り浸っている。ここはゲームコンソール関連のフォーラムの中では世界で一番参加者の平均年齢並びに精神年齢が 高いのではないかと思われる所で、PS3やXbox 360の開発者もよく投稿しており、メモリのレイテンシにまで気を配るハードウェアに密着したプログラミングの話題は非常に勉強になる。ゲーム機の話題は技術的な物も含め日本発のものも多く、私はよく<a href="http://www.gamasutra.com/php-bin/news_index.php?story=9642" target="_blank">日本語記事の英訳を提供している</a>のでそれが各方面から喜ばれているようだ。<a href="http://www.beyond3d.com/" target="_blank">Beyond3D</a>自体はリアルタイム3Dグラフィックスハードウェア専門サイトで、業界人御用達のような場所である。<br />
<br />
IRCも約2年前からあまりやっていない。今でも海外のネット上の情報の一番濃い部分がIRCにあるという点は変わらないが、それでもWebのフォーラムが爆発的に普及してきているという印象がある。6、7年前は、日本の方が掲示板は進歩していたと思う。ところがそれ以降は海外の掲示板がCMSの進歩やPHPの台頭と相俟ってWebサイトの必需品となり、発展してきた。 <a href="http://imageshack.us/" target="_blank">imageshack</a>とか<a href="http://www.imagevenue.com/" target="_blank">imagevenue</a>のようなイメージホスティングサービスも掲示板貼り付けを前提としてPHP掲示板用のBBコードを用意してくれる。海外の掲示板カルチャーは確固たるメンバー管理システムと組み合わさっていて、運用にはデータベースが必須である。CMSと親和性が高いのも当たり前だ。日本に見られる2chのような一様な匿名空間やSNSと称した出会い系サービスの作る空間、blogの散漫な空間とは一線を画した、社会的でかつ開かれた議論を行うための空間がそこにはある。<br />
<br />
こうなってくると、TCP/IPに乗せるのはHTTP だけで良く、IRC等は要らないのではないかという疑念も生じるかも知れない。しかし、WebフォーラムとIRCの微妙な差異は依然として残っており、通信のオーバーヘッドの多寡を除けば、その差異とは、IRCではチャンネルのメンバーリストがリアルタイムに更新されメンバーの出入りがリアルタイムに宣言されるという点である。IRCはメンバーのプレゼンスをも模している。この参加者の居るライブ感という差異が残る限り、IRCが滅びることはない。一方で、<a href="http://www.secondlife.com/" target="_blank">Second Life</a>や<a href="http://www.worldofwarcraft.com/" target="_blank">World of Warcraft</a>のような3Dの専用クライアントによるリアルタイムコミュニケーションも、10年前には失敗したが、回線品質が向上した現在はクリティカルマスに達しようかというところまで来ているようだ。それでも、無駄な部分を省いたコミュニケーションの需要は残るはずであり、むしろ参入障壁が十分に低ければシンプルなものがリッチなものを出し抜く可能性が十分にある。<a href="http://zzz.zggg.com/2006/11/12/server-push-today-marginal-ajax/">Web IRCクライアント</a>にはそのような展望も込めている。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-2918652791898078052006-11-15T15:55:00.000+09:002015-02-26T02:41:34.503+09:00Perl, Ruby, Multithreading, EmbeddingFor the first half of this article the main topic is multithreading in the 2 scripting languages, Perl and Ruby. By writing a multithreaded download manager application in Perl and then porting it to Ruby, it'll show you how to write a multithread application in the both languages and show you the difference of these 2 languages in this area. This section should be fairly easy and doesn't require much knowledge about the scripting languages, but it's expected that you have basic grasp of multithread programming.<br/><br/>The second half is for a bit more advanced programming topic; it's about how to write a C++ application with an embedded Perl or Ruby interpreter. Simply embedding them is not rocket science, but using them in an effective manner is not a very easy task right now because of the implementations of these languages. If you are familiar with .NET you might know it's embed-friendly with AppDomain and COM interfaces. On the other hand you have only raw C interfaces for these scripting languages, let alone scarce documents. As for Perl embedding, the sample code is based on the version I actually implemented in the web server of the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a>. Since it's realized by the mixture of C++ code and Perl hack, it requires some knowledge of C/C++, advanced Perl programming, and Perl internals. But don't be scared, I'll annotate most lines in the code to make it useful for as many people as possible because it's the very purpose of this article! Last but not least, the platform for those experiments is Microsoft Windows XP and Visual C++ 7.1. But due to the platform-neutral nature of these scripting languages most things should be applicable to any platforms.<br/><a name='more'></a><br/>Though Microsoft Windows is my primary desktop, I still use the <a href="http://www.perl.com/" target="_blank">Perl</a> scripting language to make a code snippet for a trivial text manipulation task such as list generation or some network test. I know the <a href="http://www.ruby-lang.org/" target="_blank">Ruby</a> object-oriented scripting language has become one of the hot topics among tech-savvy people, but I'd prefered Perl just because I was familiar with it for longer time through developing Perl CGI applications in the earlier part of my programming history. My most preferred programming language is by far C++, but it's not exactly handy to write a tiny application. I like C# and .NET too, but it's still overkill. Dynamic languages have certain drawbacks and I would like to criticise them, but it's true dynamic languages are sweet.<br/><br/>There are many scripting languages, Perl, Ruby, Python, PHP... First Python is out, I don't like significant whitespaces because I add lots of indents for an aesthetic reason only. Next PHP is dropped, its focus seems to be the companionship with Apache and nothing else. Ruby is cool, but I didn't like the begin...end block. Braces can be used as blocks, but not in other places. Basically I'm too fond of the C-style (or Algol-style) syntax. Perl 5 is a clumsy procedural scripting language built on the C-like syntax, and the only point I applaud in Perl is that it recommends the C-like syntax (albeit with a little difference here and there) if you try to be consistent. If I could use Ruby with C/Java like syntax I'd completely abandon Perl anyday. But it didn't happen.<br/><br/>Besides, object-orientedness in Ruby doesn't matter in a small code snippet, since humans are not that dumb and can use non-object-oriented, non-intuitive expressions. Well it's not totally useless, but it makes more sense in the other situations. When you are building a huge software stack or a huge loosely-coupled software network, capsulation is very important. There are other goodies in Ruby, but I couldn't care less about esoteric syntax sugars. The only one left is Perl. Is it the perfect language for writing a code snippet? It has to pass an actual test to see how perfect it is anyway.<br/><br/>One day I noticed my favorite download manager application for Windows didn't support chunked HTTP transfer properly. The downloader / web site grabber application, Irvine, is an excellent Japanese software but the development halted a few years ago. Its document said it would support it in future, but it's seemed that it wouldn't arrive for a while. If I have enough time I want to create an HTTP download manager just for myself in C# or something, but it's not possible at that time due to time constraints. The functionality I needed was to download many files off a web site that is configured to send all files in chunked transfer. Then I began evaluating a network library of Perl to see whether it supports chunked transfer. If it does I can just write a short Perl script. But it doesn't end there, as I had to download hundreds of files it's preferable that it downloads multiple files simultaneously just like the aforementioned Windows application. I knew Perl had implemented multithreading years ago, but still had a vague impression that it might be awkward. So this is a nice occasion to make an experiment on these 2 points: HTTP networking and multithreading.<br/><br/>Thinking about them, it came to my mind that I'd read somewhere that Ruby supported them out of the box. Ruby supports multithreading even in DOS by its own non-native threads. Also Ruby's library is supposed to be able to handle networking with ease as far as I know. I'd known how Ruby works and what kind of things are available for it for years but didn't write a Ruby code just because Perl was sufficient for my use. On the other hand the transition from Perl 5 to Perl 6 seemed not exactly smooth and I'd waited years for it to come but eventually the whole Perl scene got out of my interest when I was into other things. So this is a good occasion to evaluate Ruby for myself and see how my prejudice against it can stand.<br/><br/>So let's write a multithreaded download manager for Perl. First, we have to choose how to download files. The goal is to get an application that can handle chunked transfer properly and just that. Before writing a complicated network negotiation with basic Perl Socket classes, it's safe to search what can be done with preexisting tools. libwww-perl (LWP) is a set of Perl modules which provides API to write a web client. Fortunately it could download a file from the web site from which the Windows downloader application failed to download a file. If the standard Perl distribution for Windows available from <a href="http://www.activestate.com/" target="_blank">ActiveState</a> doesn't have LWP, you have to download it from the <a href="http://search.cpan.org/%7Egaas/" target="_blank">author's page</a> at the CPAN and build it with the nmake of Visual C++ (probably freely available from Microsoft) in a command console. libwww-perl gives you a web client object to surf the web.<br/><br/>Another thing that needs clarification is how Perl's multithreading works. <a href="http://perldoc.perl.org/" target="_blank">Perldoc</a> has a tutorial for Perl multithreading, <a href="http://perldoc.perl.org/perlthrtut.html" target="_blank">perlthrtut</a>. This document carries most of the info you need to program a Perl multithread application. If there's only one point specific to the Perl thread, it's that memory space is not shared by threads unlike usual thread implementations. Most of primitives employed in multithreaded programming are available in there. For more info about thread-related modules in Perl such as threads::shared, see their documents from the <a href="http://search.cpan.org/%7Ejdhedden/" target="_blank">author's page</a> at CPAN.<br/><br/>By the way I first tried to make my downloader application in this way - it spawns multiple worker threads from the main thread and suspends the main thread by putting a blocking semaphore (by setting the counter value) in it, then a worker thread downloads a file into the download queue, and it spawns another thread at the end of a thread. When the queue becomes empty the semaphore in the main thread is lifted, then all threads are joined and the main thread ends. The total number of worker threads is constant throughout execution. But it didn't work as expected, for some unknown reason the Perl interpreter crashes at the end of execution everytime I run it though it can download all the files in the list. If what you are writing is a native multithread application for Microsoft Windows this should work, but in Perl's case you have to join all threads without spawning a new thread from a running thread. I first thought it's caused by a bug in the thread module and browsed the <a href="http://www.cpanforum.com/dist/threads" target="_blank">forum</a> for the module. What I learned there was some modules are not thread-safe and can't be used in Perl threads. libwww-perl looks like one of them and if I removed it the program could end without an error. Seeking the solution I downloaded the latest version of the threads module from the <a href="http://search.cpan.org/%7Ejdhedden/" target="_blank">author's page</a> and built it. I tested threads-1.42 which was the latest version. It had a small problem when compiled with VC++, though. It couldn't detect the existence of a C compiler even though I used the command console that came with the Microsoft Platform SDK. The solution is to edit Makefile.pl and skip the have_cc subroutine. But it crashed too. Without a choice I abandoned this initial design and decided to use a more traditional, pthread-esque design.<br/><br/>It spawns worker threads and they get in infinite loops that pick up a job from a job queue one by one. The main thread blocks by calling the <code>join</code> function on these worker threads. This looks fairly simple even from the description of it and may look better than the one I described in the previous paragraph, but I prefer a more asynchoronous style if it works. I wrote it and this time it worked flawlessly without a crash or resource leaks. It's tested with the Win32 version of Perl 5.8.8 available from <a href="http://www.activestate.com/" target="_blank">ActiveState</a>, threads-1.42 module, and libwww-perl-5.805 module. It begins with a simple user configuration section.<br/><br/><pre class="brush: cpp"><br/># user configuration begin ################################<br/><br/># You have to list the URLs for the files to be downloaded in a file named<br/># "download_files.txt" and put it in the same directory as this script.<br/><br/>my $username = "";<br/>my $password = "";<br/><br/>my $number_of_threads = 3;<br/>my $download_interval_sec = 3;<br/><br/>my $download_list_filename = "download_files.txt";<br/><br/>my $storage_directory = 'E:\program\src\downloader\store'; # set "" for the current directory<br/><br/>my $user_agent_string = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)";<br/><br/># user configuration end ##################################<br/> </pre><br/><br/><code>$username</code> and <code>$password</code> can be set for basic authentication at password-protected pages. <code>$number_of_threads</code> tells how much worker threads are executed to process downloading tasks simultaneously. It's also possible to set how many seconds it waits before making a new download connection in <code>$download_interval</code>.<br/><br/>After user configuration options, necessary modules are loaded along with other tools useful throughout this application.<br/><br/><pre class="brush: cpp"><br/>use strict;<br/>use warnings;<br/><br/>use threads;<br/>use threads::shared;<br/>#use Thread::Queue;<br/>use Thread::Semaphore;<br/><br/>use LWP::UserAgent;<br/>use Cwd;<br/><br/>my $current_directory = Cwd::getcwd();<br/><br/>if (!$storage_directory)<br/>{<br/> $storage_directory = $current_directory;<br/>}<br/><br/>$| = 1;<br/><br/>my @download_queue : shared; # used for optimization instead of Thread::Queue<br/><br/>my $sem_download_queue = new Thread::Semaphore;<br/>my $sem_stdout = new Thread::Semaphore;<br/><br/>my $last_download_time : shared = 0;<br/> </pre><br/><br/>The <code>strict</code> and <code>warnings</code> modules are in action here to ban obscure expressions which are often seen in Perl hacks. While this is a relatively short script it's always nice that minor things don't bother users. <code>$| = 1</code> expression is what you always see in Perl scripts that manage realtime I/O. It prevents buffering and forces Perl to issue more I/O calls. <code>@download_queue</code> is the synchoronized download queue where all jobs are put and fetched from in the serial order. To make it accessible from multiple threads, it has to be marked explicitly with the <code>shared</code> keyword. This is the Perl-specific way of thread programming I wrote above. Actually Perl already has a useful tool for a situation like this as the <code>Thread::Queue</code> module though it's absent in this example to control the scope of synchonization explicitly for a bit better performance. So it requires a semaphore object as a synchronization primitive, <code>$sem_download_queue</code>. <code>$sem_stdout</code> is the semaphore to synchronize the standard output, since multiple threads try to print text reports simultaneously. <code>$last_download_time</code> is the variable to force threads to put an interval between downloads as explained in the configuration options.<br/><br/>The next part is the main loop of the script.<br/><br/><pre class="brush: cpp"><br/>print "Perl threads version: " . $threads::VERSION . "\n";<br/><br/>open(IN, $current_directory . "\\" . $download_list_filename) || die("Can't open " . $download_list_filename);<br/><br/>while (<IN>)<br/>{<br/> chomp;<br/> if ($_ =~ /^http:/i)<br/> {<br/> push @download_queue, $_;<br/> }<br/>}<br/><br/>close(IN);<br/><br/>print @download_queue . " URIs have been loaded from the download list\n";<br/><br/>for (my $i = 0; $i < $number_of_threads; ++$i)<br/>{<br/> new threads(\&amp;download_thread_func);<br/>}<br/><br/>foreach my $thr (threads->list)<br/>{<br/> if ($thr->tid)<br/> {<br/> $thr->join;<br/> }<br/>}<br/><br/>print "Download completed\n";<br/><br/>################################################################<br/> </pre><br/><br/>It's very short. It's cooler that there are less things to tweak than to be error-prone. It reads the content of the URL list and puts all of them in the download job queue. Then spawns worker threads, and waits them by calling <code>join</code>. As the <code>join</code> function blocks, the main thread can stay in memory waiting all worker threads to return from the work. If it didn't block, the main thread would just exit and the whole program would halt while leaking thread resources.<br/><br/>A spawned worker thread calls the function referenced as <code>download_thread_func</code>. Let's see the part that does the heavy work.<br/><br/><pre class="brush: cpp"><br/>sub print_ts<br/>{<br/> $sem_stdout->down;<br/> my $tid = threads->self->tid();<br/> print ($tid . ": " . shift @_);<br/> $sem_stdout->up;<br/>}<br/><br/>sub download_thread_func<br/>{<br/> for (;;)<br/> {<br/> my $u = "";<br/> my $sleep_time = 0;<br/><br/> $sem_download_queue->down;<br/><br/> if (@download_queue == 0)<br/> {<br/> $sem_download_queue->up;<br/> print_ts "The download queue is empty\n";<br/> return;<br/> }<br/> else<br/> {<br/> $u = shift @download_queue;<br/><br/> # $last_download_time is protected by $sem_download_queue<br/> my $t = time();<br/> if ($t < $last_download_time)<br/> {<br/> $sleep_time = $last_download_time - $t + $download_interval_sec;<br/> }<br/> else<br/> {<br/> if ($t - $last_download_time < $download_interval_sec)<br/> {<br/> $sleep_time = $download_interval_sec - ($t - $last_download_time);<br/> }<br/> $last_download_time = $t + $sleep_time;<br/> }<br/> }<br/><br/> $sem_download_queue->up;<br/><br/> if ($sleep_time)<br/> {<br/> print_ts "Sleeping for " . $sleep_time ." seconds\n";<br/> sleep($sleep_time);<br/> }<br/><br/> download_uri($u);<br/> }<br/>}<br/> </pre><br/><br/><code>print_ts</code> is just a debug output function mainly used to show HTTP headers. It's a print functon synchronized by a semaphore. The <code>download_thread_func</code> subroutine fetches a job from the job queue then consumes it. It continues this behavior until the queue gets empty. The part that tests and modifies the download queue is synchronized by a single semaphore. An actual download task is handled by the <code>download_uri</code> subroutine described below.<br/><br/><pre class="brush: cpp"><br/>sub download_uri<br/>{<br/> my $uri = shift @_;<br/><br/> print_ts "Downloading $uri\n";<br/><br/> my $ua = new LWP::UserAgent;<br/> $ua->cookie_jar({});<br/><br/> my $req = new HTTP::Request(GET => $uri);<br/> $req->header(<br/> "User-Agent" => $user_agent_string,<br/> "Accept" => 'image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*',<br/> "Accept-Charset" => 'iso-8859-1,*,utf-8',<br/> "Accept-Language" => 'en-US'<br/> );<br/><br/> if ($username)<br/> {<br/> $req->authorization_basic($username, $password);<br/> }<br/><br/> my $response = $ua->request($req);<br/><br/> if ($response->is_success)<br/> {<br/> print_ts "header begin-----------------------------------------\n"<br/> . $response->headers_as_string<br/> . "header end-------------------------------------------\n";<br/><br/> my $is_chunked = (<br/> ($response->header('Transfer-Encoding') &amp;&amp; $response->header('Transfer-Encoding') =~ /Chunked/i)<br/> || ($response->header('Client-Transfer-Encoding') &amp;&amp; $response->header('Client-Transfer-Encoding') =~ /Chunked/i)<br/> )<br/> ? 1 : 0;<br/><br/> if ($response->header('Content-Length') &amp;&amp; $response->header('Content-Length') == 0 &amp;&amp; !$is_chunked)<br/> {<br/> print_ts "Content-Length is zero\n";<br/> }<br/> else<br/> {<br/> my $image_name = "tmp.bin";<br/> if ($uri =~ /\/([^\/]+)$/)<br/> {<br/> $image_name = $1;<br/> }<br/><br/> if ($response->header('Content-Disposition'))<br/> {<br/> if ($response->header('Content-Disposition') =~ /filename=(.+)/i)<br/> {<br/> $image_name = $1;<br/> $image_name =~ s/"//g;<br/> }<br/> }<br/><br/> if (!open(OUT, ">$storage_directory\\$image_name"))<br/> {<br/> print_ts "open error : $storage_directory\\$image_name\n";<br/> exit();<br/> }<br/> else<br/> {<br/> binmode(OUT);<br/> #my $fsize = $response->header('Content-Length');<br/> if (!defined(syswrite OUT, $response->content, length($response->content)))<br/> {<br/> print_ts "syswrite error : $storage_directory\\$image_name\n";<br/> }<br/> syswrite OUT, $response->content, length($response->content);<br/> close(OUT);<br/><br/> print_ts "Downloaded $image_name\n";<br/> }<br/> }<br/> }<br/> else<br/> {<br/> my $st = $response->status_line;<br/> print_ts "Error: $uri : $st\n";<br/> }<br/>}<br/> </pre><br/><br/>It instantiates an object of the <code>LWP::UserAgent</code> class to do HTTP negotiation and sets necessary HTTP headers for it. After a request is sent, the object receives an HTTP response header and body. Since the purpose of this script is to download a file off a server that does chunked transfer, it searches an HTTP header line that indicates it, <code>Transfer-Encoding</code> or <code>Client-Transfer-Encoding</code>. In a chunked transfer, a real file name is communicated by a <code>Content-Disposition</code> header, so it scans this header too. When all necessary info are available the rest thing to do is to save received data into a file. If it's a binary file, you have to call <code>binmode</code> to set the file handle in the binary mode on Windows. The data length can be obtained by a <code>Content-Length</code> header for a nomal HTTP response but the chunked transfer is the method to send a file without sending a <code>Content-Length</code> header. Conveniently the data size is already known by the length of <code>$response->content</code> data.<br/><br/>That's all for the Perl downloader script. It's about 200 lines and it works, no big deal. So let's move onto the next task, porting it to Ruby.<br/><br/>Before writing the code, since Japanese is my 1st language I went to the Japanese side of the Ruby HQ, <a href="http://www.ruby-lang.org/" target="_blank">ruby-lang.org</a> for the Ruby language/library reference. It's known that Ruby was invented in Japan and some non-Japanese people often complain help documents of Ruby are rather weak compared to Perl or Python in a language comparison war. But the reality is, the Japanese documents for Ruby are downright terrible. At least the official manual is very sparse and unorganized. Probably mailing lists and code samples in Japanese may carry beefier contents than English equivalents, but the Japanese documents at <a href="http://www.ruby-lang.org/" target="_blank">ruby-lang.org</a> are not something you yearn for and I actually found English documents linked from the <a href="http://www.ruby-lang.org/en/documentation/" target="_blank">English side</a> are more useful.<br/><br/>Writing a Ruby code immediately after writing a Perl code is a bit puzzling experience. In Ruby, the <code>@</code> prefix means an instance variable (or an object property, in a more usual object-oriented lingo) unlike the array expression in Perl. In Ruby it's about a variable scope and in Perl it's about a data type. I like Ruby's scope which is more object-oriented and cleaner than Perl's one which is heavily dependent on its symbol table and often the source of dirty hacks. But the main problem is besides that. It's about braces. As I wrote at the beginning of this article I like braces and would like to use it as much as I can do. But Ruby only allows it at blocks and I often trip on it by using braces where it accepts only <code>do...end</code>. Another thing is, Ruby doesn't require you to end a line with a semicolon, but when you put an unwanted carriage-return it just emits syntax errors and stops. So you can't hit the Enter key just to tidy up the layout. It is especially problematic when you declare a block parameter for a block which is one of the main selling points of Ruby. So my advice for writing a Ruby code is: don't use braces and don't hit the Enter key too often.<br/><br/>Let's take a look at the Ruby version. It has the user configuration part at the beginning and it won't need much explanation as it's almost identical to the Perl version. Some variables are declared as instance variables of the main function to make them available in methods. In the Ruby version, HTTP negotiation is handled by the standard <code>net/http</code> library which, fortunately again, can download chunked-transferred files. <code>thread</code> is Ruby's intrinsic class for multithread programming. <code>uri</code> is a small utility class to handle a URI. <code>Net::HTTP.version_1_2</code> is the pragma to instruct <code>net/http</code> to use the newer implementation. <code>if $0 == __FILE__ line</code> is an include guard but it has no influence in this demo. This script is tested with Ruby 1.8.5.<br/><br/><pre class="brush: cpp"><br/><br/># user configuration begin ################################<br/><br/>@username = ""<br/>@password = ""<br/><br/>number_of_threads = 3<br/>@download_interval_sec = 3<br/><br/>download_list_filename = "download_files.txt"<br/><br/>@storage_directory = 'E:\program\src\downloader\store' # set "" for the current directory<br/><br/>@user_agent_string = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"<br/><br/># user configuration end ##################################<br/><br/>require 'net/http'<br/>require 'thread'<br/>require 'uri'<br/><br/>Net::HTTP.version_1_2<br/><br/>if $0 == __FILE__<br/></pre><br/><br/>The following section contains the Ruby version of the functions with the same name as in the Perl version. The only difference is it uses synchronization primitives (mutex) more intuitively.<br/><br/><pre class="brush: cpp"><br/>def puts_ts(x)<br/> @mutex_pt.synchronize do<br/> puts Thread.current.object_id.to_s + ": " + x<br/> end<br/>end<br/><br/>def download_thread_func<br/> while true do<br/> u = ""<br/> sleep_time = 0<br/><br/> @mutex_dq.synchronize do<br/> if @download_queue.empty?<br/> puts_ts "The download queue is empty"<br/> return<br/> else<br/> u = @download_queue.shift<br/> # @last_download_time is protected by @mutex_dq<br/> t = Time.now.gmtime.to_i<br/> if t < @last_download_time<br/> sleep_time = @last_download_time - t + @download_interval_sec<br/> else<br/> if t - @last_download_time < @download_interval_sec<br/> sleep_time = @download_interval_sec - (t - @last_download_time)<br/> end<br/> @last_download_time = t + sleep_time<br/> end<br/> end<br/> end<br/><br/> if sleep_time != 0<br/> puts_ts "Sleeping for " + sleep_time.to_s + " seconds"<br/> sleep sleep_time<br/> end<br/><br/> download_uri u<br/> end<br/>end<br/></pre><br/><br/>The next section is again the function with the same name as the Perl version, but its content is a little different.<br/><br/><pre class="brush: cpp"><br/>def download_uri(uri)<br/><br/> puts_ts "Downloading #{uri}"<br/><br/> req = Net::HTTP::Get.new(URI.parse(uri).path)<br/><br/> req["User-Agent"] = @user_agent_string<br/> req["Accept"] = 'image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*'<br/> req["Accept-Charset"] = 'iso-8859-1,*,utf-8'<br/> req["Accept-Language"] = 'en-US'<br/><br/> req.basic_auth @username, @password unless @username.empty?<br/><br/> begin<br/> Net::HTTP.start(URI.parse(uri).host, 80) do |http|<br/> http.request(req) do |res|<br/> h = "#{res.code} #{res.message}\nheader begin-----------------------------------------\n"<br/><br/> is_chunked = false<br/> filename = ""<br/> res.canonical_each do |n, v|<br/> h += (n + ": " + v + "\n")<br/> if n =~ /Transfer-Encoding/i &amp;&amp; v =~ /Chunked/i<br/> is_chunked = true<br/> end<br/> if n =~ /Content-Disposition/i &amp;&amp; v =~ /filename=(.+)/i<br/> filename = $1<br/> filename.gsub(/^"|"$/, '')<br/> end<br/> end<br/> h += "header end-------------------------------------------"<br/> puts_ts h<br/><br/> unless res.code == "200"<br/> return<br/> end<br/><br/> if filename == "" &amp;&amp; uri =~ /\/([^\/]+)$/<br/> filename = @storage_directory + "\\" + $1<br/> end<br/><br/> open(filename, "wb") do |file|<br/> res.read_body do |str|<br/> file.write str<br/> end<br/> end<br/> end<br/> end<br/> rescue Exception => e<br/> puts_ts e.to_s<br/> end<br/><br/>end<br/></pre><br/><br/>By processing a code block the Net::HTTP class can process an incoming HTTP response-body stream in multiple small chunks as the <code>read_body</code> function of the <code>Net::HTTPResponse</code> class returns. It means you don't have to store the whole received file in memory before dumping it onto a file unlike the Perl version. When a downloaded file is too large this is necessity.<br/><br/>The rest covers the main function. I think you can see the pattern here and it needs no further explanation. <code><<</code> is a useful overloaded operator to push an element to the end of an array just like stack manipulation.<br/><br/><pre class="brush: cpp"><br/># main begin #############################################################<br/>@mutex_pt = Mutex::new<br/><br/>if @storage_directory.empty?<br/> @storage_directory = Dir.pwd<br/>end<br/><br/>@download_queue = []<br/>@mutex_dq = Mutex::new<br/>@last_download_time = 0;<br/><br/>File.foreach(download_list_filename) {|line|<br/> @download_queue << line.chomp if line.match(/^http:/i)<br/>}<br/><br/>threads = []<br/><br/>for i in 1..number_of_threads<br/> threads << Thread::start do download_thread_func end<br/>end<br/><br/>for i in 1..number_of_threads<br/> threads[i - 1].join<br/>end<br/><br/># main end ###############################################################<br/>end<br/></pre><br/><br/>That's all for the Ruby version.<br/><br/>Now how do they compare? The first thing to be noticed is the script length. The Perl version is over 200 lines and 4,890 bytes. The Ruby version is under 150 lines and 3,383 bytes. The second point is the performance. Squeezing extra performance is the only reason why they are multithreaded. I used them to download dozens of files and the Perl version took up about 15MB RAM and the Ruby version occupied only 5MB. This is probably due to the difference of downloading methods between 2 network libraries (whole download vs partial), but also due to the implementation of interpreter threads in these languages, since 15MB RAM usage is a bit too high for downloading files around 100KB. The speed seemed a tie, but in some cases the Perl version was faster and the Ruby version stuttered. Probably it can be attributed to file I/O and some context-switching awkwardness in the Ruby version.<br/><br/>My final verdict on this subject is, Ruby is the winner, contrary to some of the possible doubts I suggested above. Of course I still don't like <code>begin...end</code>, but if you really like the way Java programs are written I guess you like Ruby too. Ruby is even nicer than Java. Basically there's nothing Ruby can't do except for Perl-specific symbolic hacks. Why not just jump ship and switch to Ruby for good?<br/><br/>But this discourse doesn't end there. Ruby is certainly a new kid on the block (well not very new actually but leave it at that now) that presents great opportunities. Even then, there may be something useful in an older and proven tool. My server application, <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a>, ended up with an embedded Perl interpreter instead of Ruby. <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> has already had a Common Language Runtime (CLR) embedded into it since years ago, but for the upcoming new version that took over a year in preparation I planned to add something new for another bullet point for the update list. Though at first I wanted to embed PHP which is fairly popular for a web application because of the affinity with the Apache HTTPd, I abandoned that idea for now. The information about PHP embedding seemed scarce on the web. (BTW I downloaded the source code of the PHP5 and browsed it, it's not much larger than the source code of the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> in size which was a surprise for me.) So the candidates are, as you expect, Ruby and Perl. What I'd like to accomplish is embedding an interpreter in the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> and making it process a web application, but not executing it for every request ala CGI. However, the merit of including such an interpreter is that it can support existing applications available without writing a new code. The <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> has a CLR embedded in it to execute a web application, but it requires a user to write a stub code, or an entire application. This time I want existing CGI applications to run on the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a>. Therefore the goal is to add <a href="http://perl.apache.org/" target="_blank">mod_perl</a> or <a href="http://www.modruby.net/" target="_blank">mod_ruby</a> like capability to it. The requirements are<br/><br/><a href="../../dice/"> </a>1. The interpreter is persistent.<br/>2. All input from the standard input and all output to the standard output must be hooked by the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> because stdin/stdout are the points where CGI scripts interact with an internet user through a web server.<br/><br/>This section begins with Ruby. Before writing a code for embedding Ruby, the license of Ruby has to be examined. It adopts the dual license by the GPL and the other BSD-like license. It seems you can do whatever with Ruby if you don't choose GPL, but in reality it's not that simple. The current Ruby implementation (1.8) contains the GNU regular expression library and it forces you to make your application GPLed if you just embed a Ruby interpreter as is. Actually there's a workaround and it's very simple. The current development version of Ruby (1.9) has the <a href="http://www.geocities.jp/kosako3/oniguruma/" target="_blank">Oniguruma</a> regular expressions library which is under the BSD license to avoid this license issue. If you choose to stick to 1.8, the source code of the Oniguruma version 2.x is available for download, it can be integrated to the Ruby 1.8 source code (the instruction is included in the Onigruma document). After building the whole Ruby, you can open the binary (msvcr71-ruby19.dll if you use VC++ 7.1 and Ruby 1.9) with <a href="http://www.dependencywalker.com/" target="_blank">Dependency Walker</a> to see if it has Onigruma symbols (prefixed with Onig) and not GNU regex ones.<br/><br/>The first thing you have to do is to download the Ruby source code at the <a href="http://www.ruby-lang.org/en/downloads/" target="_blank">download page</a> of the <a href="http://www.ruby-lang.org/" target="_blank">ruby-lang.org</a>. Then obviously you have to learn how to embed Ruby. The particular document I referred to when I wrote my sample code was magazine articles written by Shugo Maeda, the author of <a href="http://www.modruby.net/" target="_blank">mod_ruby</a>. They are <a href="http://shugo.net/article/cmagazine/3rd/" target="_blank">here</a> and <a href="http://shugo.net/article/cmagazine/4th/" target="_blank">here</a> at Mr. Maeda's web site, and his VIM patch is <a href="http://shugo.net/archive/vim-ruby/" target="_blank">here</a>. They show how he embedded Ruby into the vi-clone editor VIM. Unfortunately they are in Japanese though some code samples are also available with them. And this is the real horror story, I couldn't find other detailed Japanese documents that are helpful in embedding Ruby. Actually the English <a href="http://wiki.rubygarden.org/Ruby/page/show/EmbedRuby" target="_blank">EmbedRuby</a> article at the Ruby Garden is probably the second best source of info on this matter. After reading the tutorial, you can download the C source code of <a href="http://www.modruby.net/" target="_blank">mod_ruby</a> to learn how a real-world application hosts a Ruby interpreter though it may be hard for those who are not familiar with how an Apache module works.<br/><br/>In addition to the basics of embedding Ruby, Maeda's article offers how to interrupt the standard output of Ruby and trap it to feed to VIM instead of the console screen. Ruby's outputs are all assigned in the <code>$></code> special variable and its native C implementation is the object named <code>rb_defout</code>. By hooking the <code>write</code> method of this object you can trap all Ruby outputs to the standard output. But how? It's possible by the <code>rb_define_singleton_method</code> function defined in the class.c of the Ruby source code. It defines a "singleton method" which is a special method for a special object, which is <code>rb_defout</code> in this case. Also each output method in Ruby can be redefined by the <code>rb_define_global_function</code> function. The source code of <a href="http://www.modruby.net/" target="_blank">mod_ruby</a> shows that it actually redefines all related methods in Ruby with respective hook functions. <code>VALUE</code> is a Ruby type which can hold any Ruby object. It resembles the <code>VARIANT</code> type in COM, or the void* type in C.<br/><br/><pre class="brush: cpp"><br/>-- class.c --<br/>void<br/>rb_define_singleton_method(VALUE obj, const char *name, VALUE (*func)(ANYARGS), int argc)<br/>{<br/> rb_define_method(rb_singleton_class(obj), name, func, argc);<br/>}<br/><br/>void<br/>rb_define_method(VALUE klass, const char *name, VALUE (*func)(ANYARGS), int argc)<br/>{<br/> rb_add_method(klass, rb_intern(name), NEW_CFUNC(func, argc), NOEX_PUBLIC);<br/>}<br/><br/>void<br/>rb_define_global_function(const char *name, VALUE (*func)(ANYARGS), int argc)<br/>{<br/> rb_define_module_function(rb_mKernel, name, func, argc);<br/>}<br/>-- eval.c --<br/><br/>void<br/>rb_add_method(VALUE klass, ID mid, NODE *node, int noex)<br/>{<br/><br/>}<br/></pre><br/><br/>To build a C++ code with a Ruby interpreter embedded, Ruby header files, a Ruby static library and dynamic link library are required. The header file, <code>ruby.h</code>, is found in the root directory of the Ruby source code archive. The static/dynamic libraries can be generated by building the Ruby source code. To build it on Windows, open a VC++/Platform SDK build environment console and run the <code>configure.bat</code> in the <code>win32</code> directory then type <code>nmake</code>. You get <code>msvcr71-ruby19.lib</code> as a static library and <code>msvcr71-ruby19.dll</code> as a dll. Note that it doesn't produce a static library for static linking. These are release builds that refer to the release version of the MS C-runtime library. I tried to build a debug version by editing the makefile (changing the compiler option /MD to /MDd) but the build process stopped in building one of .exe. It seemed to have created libraries at least, but I couldn't link it with a compiled C++ code in the Debug mode nor in the Release mode.<br/><br/>Once the Ruby library is ready you have to set up a build environment for a Ruby-embedded application. Set the root directory of the Ruby source code in your include path and set the library path in your LIB directory (In VC++ it's in Tool | Options | Projects | VC++ Directories | Library files). As the resulted executable requires <code>msvcr71-ruby19.dll</code> to run it should be copied into the working directory. As for the C-runtime library, you have to choose a multi-threaded DLL, since Ruby's memory manager is built with a multithread version of the library. Unfortunately the library built by the default makefile has conflicts with other libraries in the Debug mode with the multi-threaded debug DLL (/MDd). This problem can be addressed by putting <code>libcmtd.lib</code> in Linker | Input | Ignore Specific Library in the VC++ project options for the Debug mode. In the Release mode it's not required, but it still issues linker warnings LNK4049. I tried to create a project file by myself but failed to do that as the Ruby I built couldn't get the current directory as it can't access some APIs. I hope these issues with Windows are resolved in future.<br/><br/>The environment is ready, let's see the actual C++ code. This sample code is written for a Ruby 1.9 development snapshot and doesn't work with Ruby 1.8 as there are significant changes in the Ruby source code in 1.9. What this code does is fairly simple, initializes a Ruby interpreter, loads and executes a Ruby script, then destroys the interpreter. It includes necessary files at the beginning. For the sake of simpleness I included the library file by the pragma, but it may be safe to do it in the project settings if your project contains many files that may conflict with it. (In the case of Perl embedding, it didn't work.) In addition to the <code>ruby.h</code>, standard C++ headers are included for utility functions.<br/><br/><pre class="brush: cpp"><br/>#pragma comment(lib, "E:\\program\\lib\\rubylib\\ruby_win32\\usr\\lib\\msvcr71-ruby19-static.lib")<br/><br/>extern "C" {<br/><br/>#include <ruby.h><br/><br/>}<br/><br/>#include <iostream><br/>#include <string><br/><br/>using namespace std;<br/></pre><br/><br/>First, output hook functions are defined.<br/><br/><pre class="brush: cpp"><br/>VALUE ruby_write_hook(VALUE self, VALUE str)<br/>{<br/> str = rb_obj_as_string(str);<br/> cout << string(RSTRING_PTR(str), RSTRING_LEN(str));<br/><br/> return Qnil;<br/>}<br/><br/>VALUE ruby_p_hook(int argc, VALUE *argv, VALUE self)<br/>{<br/> VALUE str = rb_str_new("", 0);<br/><br/> for (int i = 0; i < argc; i++)<br/> {<br/> if (i > 0)<br/> rb_str_cat(str, ", ", 2);<br/><br/> rb_str_concat(str, rb_inspect(argv[i]));<br/> }<br/><br/> cout << RSTRING_PTR(str);<br/><br/> return Qnil;<br/>}<br/></pre><br/><br/><code>ruby_write_hook</code> is a function to hook the singleton method of <code>rb_defout</code> as explained already. It extracts a <code>char*</code> pointer and its length from a Ruby string that is assigned to this special object, and outputs it by itself with <code>cout</code>. <code>Qnil</code> is the null value in the Ruby C implementation. <code>ruby_p_hook</code> is defined to redefine the kernel method <code>p</code> which can print the state of an object in a human readable form like the <code>ToString</code> method in C#/Java. It creates a new Ruby string by <code>rb_str_new</code> and enumerates its elements by adding them with <code>rb_str_concat</code>.<br/><br/><code>init_ruby</code> is the function that initializes the Ruby interpreter. <code>ruby_init_loadpath</code> initializes the library path of Ruby. <code>show_error_pos</code> and <code>show_exception_info</code> are the functions that show detailed error information just like what you get when you feed an erroneous Ruby script to the Ruby interpreter.<br/><br/><pre class="brush: cpp"><br/>void init_ruby()<br/>{<br/> ruby_init();<br/> ruby_init_loadpath();<br/>}<br/><br/>void show_error_pos()<br/>{<br/> ID this_func = rb_frame_this_func();<br/><br/> if (ruby_sourcefile)<br/> {<br/> if (this_func)<br/> {<br/> cout << ruby_sourcefile << ":" << ruby_sourceline << ":in" << rb_id2name(this_func) << endl;<br/> }<br/> else<br/> {<br/> cout << ruby_sourcefile << ":" << ruby_sourceline << endl;<br/> }<br/> }<br/>}<br/><br/>void show_exception_info()<br/>{<br/> if (NIL_P(ruby_errinfo))<br/> return;<br/><br/> VALUE errat = rb_funcall(ruby_errinfo, rb_intern("backtrace"), 0);<br/> if (!NIL_P(errat))<br/> {<br/> VALUE mesg = (RARRAY_PTR(errat))[0];<br/><br/> if (NIL_P(mesg))<br/> {<br/> show_error_pos();<br/> }<br/> else<br/> {<br/> cout << string(RSTRING_PTR(mesg), RSTRING_LEN(mesg));<br/> }<br/> }<br/><br/> VALUE eclass = CLASS_OF(ruby_errinfo);<br/><br/> char* einfo;<br/> int elen;<br/> int state;<br/> VALUE estr = rb_protect(rb_obj_as_string, ruby_errinfo, &amp;state);<br/> if (state)<br/> {<br/> einfo = "";<br/> elen = 0;<br/> }<br/> else<br/> {<br/> einfo = RSTRING_PTR(estr);<br/> elen = RSTRING_LEN(estr);<br/> }<br/><br/> if (eclass == rb_eRuntimeError &amp;&amp; elen == 0)<br/> {<br/> cout << ": unhandled exception" << endl;<br/> }<br/> else<br/> {<br/> VALUE epath;<br/><br/> epath = rb_class_path(eclass);<br/> if (elen == 0)<br/> {<br/> cout << ": " << string(RSTRING_PTR(epath), RSTRING_LEN(epath)) << endl;<br/> }<br/> else<br/> {<br/> char* tail = 0;<br/> int len = elen;<br/><br/> if ((RSTRING_PTR(epath))[0] == '#')<br/> epath = 0;<br/><br/> if (tail = strchr(einfo, '\n'))<br/> {<br/> len = tail - einfo;<br/> tail++;<br/> }<br/><br/> cout << ": " << string(einfo, len);<br/> if (epath)<br/> {<br/> cout << " (" << string(RSTRING_PTR(epath), RSTRING_LEN(epath)) << endl;<br/> }<br/><br/> if (tail)<br/> {<br/> cout << string(tail, elen - len - 1) << endl;<br/> }<br/> }<br/> }<br/><br/> if (!NIL_P(errat))<br/> {<br/> const int TRACE_HEAD = 8;<br/> const int TRACE_TAIL = 5;<br/> const int TRACE_MAX = TRACE_HEAD + TRACE_TAIL + 5;<br/><br/> RArray* ep = RARRAY(errat);<br/><br/> long len = RARRAY_LEN(errat);<br/> for (int i = 1; i < len; ++i)<br/> {<br/> if (TYPE((RARRAY_PTR(errat))[i]) == T_STRING)<br/> {<br/> cout << " from " << string(RSTRING_PTR((RARRAY_PTR(errat))[i]), RSTRING_LEN((RARRAY_PTR(errat))[i])) << endl;<br/> }<br/><br/> if (i == TRACE_HEAD &amp;&amp; len > TRACE_MAX)<br/> {<br/> cout << " ... " << len - TRACE_HEAD - TRACE_TAIL << "ld levels..." << endl;<br/> i = len - TRACE_TAIL;<br/> }<br/> }<br/> }<br/>}<br/></pre><br/><br/>The last part is the function that loads and executes a Ruby script (<code>execute_ruby</code>) and the main function. It registers output hook functions in the Ruby interpreter by <code>rb_define_singleton_method</code> and <code>rb_define_global_function</code>.<br/><br/><pre class="brush: cpp"><br/>void execute_ruby(const char* pScriptName)<br/>{<br/> int state = 0;<br/><br/> extern VALUE rb_defout;<br/><br/> typedef VALUE (*rubyfunc)(...);<br/><br/> rb_defout = rb_obj_alloc(rb_cObject);<br/> rb_define_singleton_method(rb_defout, "write", (rubyfunc)ruby_write_hook, 1);<br/> rb_define_global_function("p", (rubyfunc)ruby_p_hook, -1);<br/><br/> // Always need a full path<br/> rb_load_protect(rb_str_new2(pScriptName), 0, &amp;state);<br/> if (state)<br/> {<br/> switch (state)<br/> {<br/> case 0x1: // TAG_RETURN<br/> cout << "unexpected return" << endl;<br/> show_error_pos();<br/> break;<br/> case 0x2: // TAG_BREAK<br/> cout << "unexpected break" << endl;<br/> show_error_pos();<br/> break;<br/> case 0x3: // TAG_NEXT<br/> cout << "unexpected next" << endl;<br/> show_error_pos();<br/> break;<br/> case 0x4: // TAG_RETRY<br/> cout << "retry outside of rescue clause" << endl;<br/> show_error_pos();<br/> break;<br/> case 0x5: // TAG_REDO<br/> cout << "unexpected redo" << endl;<br/> show_error_pos();<br/> break;<br/> case 0x6: // TAG_RAISE<br/> case 0x8: // TAG_FATAL<br/> show_exception_info();<br/> break;<br/> default:<br/> cout << "unknown longjmp status " << state << endl;<br/> break;<br/> }<br/> }<br/><br/> rb_gc();<br/>}<br/><br/>int _tmain(int argc, _TCHAR* argv[])<br/>{<br/> init_ruby();<br/><br/> execute_ruby("test.rb");<br/><br/> ruby_finalize();<br/><br/> return 0;<br/>}<br/></pre><br/><br/>The <code>rb_load_protect</code> function is the protected version of the <code>rb_load</code> function that can suppress exceptions. Unless there's a special reason you should always use the protected version of Ruby functions. After a script is evaluated, it shows error information if any. <code>rb_gc</code> is called at the end to invoke the garbage collector.<br/><br/>When this program is executed it loads the "test.rb" Ruby script. My "test.rb" was a simple script that does print some strings and apparently it could output characters through the redefined hook functions. I thought that my experiment was a success. But I did one more test. The new test fed the Ruby downloader script which I described above to this test code. To my surprise, it halted with the "memory error" in the Ruby interpreter. I had no idea what's wrong, but apparently the culprit was the <code>net/http</code> library. The library path is in the <code>$LOAD_PATH</code> variable, which means it's not a loading error. I tried to debug it but the Ruby library was compiled without debug symbols as explained at the beginning of this section. I was really disappointed as I thought the experiment was almost a success. Eventually I lost my interest in embedding Ruby into my application. It is known that the Ruby author himself admits that the weakness of the current Ruby implementation is in embedding because it can't have multiple interpreters side by side, but I didn't know there was such a basic issue was left. It's expected the next version of the Ruby VM addresses some of the issues in embedding, but apparently it's far off in 2008. There may be some workaround if I look more carefully into the <a href="http://www.modruby.net/" target="_blank">mod_ruby</a> source code, but I'm out of energy for further research for the time being. If you know a solution please email me, thanks in advance.<br/><br/>The last hope is naturally on Perl. First you need the Perl source code. I used a Perl development snapshot available at <a href="http://mirrors.develooper.com/perl/APC/perl-current-snap/" target="_blank">here</a> but snapshots of the development trunk in this directory often fail to build or pass the test. After looking at the latest version number of the stable release at <a href="http://www.perl.com/download.csp" target="_blank">Perl.com </a>, you may pick up the latest stable snapshot in <a href="http://mirrors.develooper.com/perl/APC/" target="_blank">another directory</a>. You can find exactly which snapshot is broken at the <a href="http://www.nntp.perl.org/group/perl.perl5.porters/">perl.perl5.porters</a> newsgroup.<br/><br/>To build it on Windows, edit the <code>Makefile</code> in the win32 direcrtory (in most cases nothing more is needed than just editing the install path and the compiler selection). Then open a VC++/Platform SDK build environment console and type <code>nmake</code> to start the building process. After it's done type <code>nmake test </code>to test it and type <code>nmake install</code> to build and install all Perl libraries and the Perl interpreter into the install path you specified in the <code>Makefile</code>.<br/><br/>General information on how to embed a Perl interpreter into your C/C++ application is available in the <a href="http://perldoc.perl.org/perlembed.html" target="_blank">perlembed</a> document. However, I recommend you to browse <a href="http://perldoc.perl.org/perlguts.html" target="_blank">perlguts</a> and <a href="http://perldoc.perl.org/perlcall.html" target="_blank">perlcall</a> before going to <a href="http://perldoc.perl.org/perlembed.html" target="_blank">perlembed</a>. Also <a href="http://perldoc.perl.org/perlapi.html" target="_blank">perlapi</a> may be useful for a reference. <a href="http://perldoc.perl.org/perlguts.html" target="_blank">perlguts</a> explains how the C implementation of Perl represents Perl's data types and subroutines in C. <a href="http://perldoc.perl.org/perlcall.html" target="_blank">perlcall</a> explains how to call Perl subroutines from C. If you still have questions after reading these documents, you can always grep the Perl source code for functions, function macros and comments.<br/><br/>For an actual embedding code sample, <a href="http://perl.apache.org/" target="_blank">mod_perl</a> is probably the most complete. If you read <code>mod_perl.c</code> in the src\modules\perl directory of mod_perl 2, you'll find the <code>modperl_response_handler_cgi</code> function as the main function that executes a Perl CGI script. The basic flow of this function is<br/><br/>1. Setup environmental variables passed to Perl<br/>2. Override Perl's standard input and standard output<br/>3. Process an HTTP request to a Perl CGI<br/>4. Restore everything back<br/><br/>Basically it's in line with what I wrote I wanted to do earlier in this article. Apache does most of these things by directly manipulating Perl by C code. But I don't need as much security as the Apache module does, since the current version of the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> doesn't support multi-user. Running multiple interpreters is not required for now since the context switching method by <code>PERL_SET_CONTEXT</code> in the current Perl implementation sucks. It's based on heavy use of macro and requires nasty macro hacks which will most likely have unwanted effects in unexpected areas in a large project. The Perl source code predetermines the name of an interpreter object as "my_perl" and predefined macros can't take other names, which means you have to assign your Perl interpreter pointer to a local variable <code>PerlInterpreter* my_perl </code>everytime you have to do something with it. Also the perl header file redefines many symbols for its own API, which means if your C++ class has a member function named <code>write</code> it conflicts with Perl. So my advice is, don't include <code>perl.h</code> in a header file that is included by many other files in your project. Include it at the beginning of a .cpp source file and put all Perl-related things in it. But how can you define a class that has a pointer to a Perl interpreter as its property? You can put a <code>char*</code> or something equivalent in the place of the pointer to an interpreter in your class declaration and cast it to <code>PerlInterpreter*</code> everytime you use it in the .cpp source file.<br/><br/>I decided to offload most of the sandbox work to Perl to keep the C++ part simple. The basic design is like this:<br/><br/>1. The Perl interpreter in the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> loads a setup Perl script which sets <code>STDIN</code>/<code>STDOUT</code> hooks and offers other necessary utilities for sandboxing<br/>2. Whenever a CGI script is called, if it's the first time, the Perl interpreter evaluates it as a subroutine in a unique package by using <code>eval</code> in Perl, then it's cached as a compiled code. If it is an already compiled code in the cache, it just returns a reference to it.<br/>3. The Perl interpreter executes a compiled Perl code by dereferencing its subroutine reference and the C++ part gets the output by reading a Perl variable to collect all Perl output. The C++ part is protected and synchronized by a critical section that keeps this Perl variable alive (though there's certain performance penalty)<br/><br/>Let's take a look at the Perl setup script (<code>EmbeddedPerlSandbox.pl</code>). This script was written for the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> but a newer version may be included in the latest package. To see how it works you should download the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> and run its web server by yourself. First, it defines the package <code>EmbeddedPerlSandboxOut</code> that intercepts and overrides the standard out (<code>STDOUT</code> in Perl). The required Perl version is specified as 5.8.0, but a lower version may work too.<br/><br/><pre class="brush: cpp"><br/># EmbeddedPerlSandbox for DICE<br/># (c) RyuK 2006 All Rights Reserved<br/>#<br/># klassphere[at.mark]gmail.com<br/># http://aiueo.da.ru/<br/># http://zzz.zggg.com/<br/>#<br/># This file is not redistributable.<br/>#<br/><br/>use 5.8.0;<br/><br/>package EmbeddedPerlSandboxOut;<br/><br/>sub TIEHANDLE<br/>{<br/> my $classname = shift;<br/> my $buffer = "";<br/> bless \$buffer, $classname;<br/>}<br/><br/>sub PRINT<br/>{<br/> my $buffer = shift;<br/> my $s = shift;<br/> $$buffer .= $s;<br/>}<br/><br/>sub PRINTF<br/>{<br/> my $buffer = shift;<br/> my $s = shift;<br/> $$buffer .= sprintf($s, @_);<br/>}<br/><br/>sub WRITE<br/>{<br/> my ($buffer, $data, $len, $offset) = @_;<br/><br/> if (!defined($len))<br/> {<br/> $len = length($data);<br/> }<br/><br/> if (!defined($offset))<br/> {<br/> $offset = 0;<br/> }<br/><br/> $$buffer .= substr($data, $offset, $len);<br/>}<br/><br/>sub READLINE<br/>{<br/> my $buffer = shift;<br/> return $$buffer;<br/>}<br/><br/>sub BINMODE<br/>{<br/> # does nothing<br/> return 1;<br/>}<br/><br/>sub CLOSE<br/>{<br/> my $buffer = shift;<br/> undef $buffer;<br/>}<br/><br/>sub DESTROY<br/>{<br/> my $buffer = shift;<br/> undef $buffer;<br/>}<br/></pre><br/><br/>When a Perl script is executed, this class is associated with <code>STDOUT</code> by the <code>tie</code> Perl function which calls the <code>TIEHANDLE</code> subroutine. It creates an object which is just a scalar value that works as a buffer to hold the content of the standard output. After that, if <code>print</code> tries to write something onto <code>STDOUT</code> the <code>PRINT</code> function in this class is called. <code>READLINE</code> is called by <code><></code>.<br/><br/>The next section defines <code>EmbeddedPerlSandboxIn</code> to override the standard input in the same way.<br/><br/><pre class="brush: cpp"><br/>package EmbeddedPerlSandboxIn;<br/><br/>sub TIEHANDLE<br/>{<br/> my $classname = shift;<br/> my $buffer = shift;<br/> bless \$buffer, $classname;<br/>}<br/><br/>sub READLINE<br/>{<br/> my $buffer = shift;<br/><br/> # substr EXPR,OFFSET,LENGTH,REPLACEMENT<br/> return (length($$buffer) ? substr(<br/> $$buffer,<br/> 0,<br/> (defined($/) ? index($$buffer, "$/"): length($$buffer) - 1) + 1,<br/> ""<br/> )<br/> : undef);<br/>}<br/><br/>sub READ<br/>{<br/> my ($buffer, $len, $offset) = ($_[0], $_[2], $_[3]);<br/><br/> if (!defined($offset))<br/> {<br/> $offset = 0;<br/> }<br/><br/> if ($len > length($$buffer))<br/> {<br/> $len = length($$buffer);<br/> }<br/><br/> # You can use the substr() function as an lvalue<br/> substr($_[1], $offset, $len) = substr($$buffer, 0, $len, "");<br/><br/> return $len;<br/>}<br/><br/>sub GETC<br/>{<br/> my $buffer = shift;<br/> return (length($$buffer) ? substr(<br/> $$buffer,<br/> 0,<br/> 1,<br/> ""<br/> )<br/> : undef);<br/>}<br/><br/>sub BINMODE<br/>{<br/> # does nothing<br/> return 1;<br/>}<br/><br/>sub CLOSE<br/>{<br/> my $buffer = shift;<br/> undef $buffer;<br/>}<br/><br/>sub DESTROY<br/>{<br/> my $buffer = shift;<br/> undef $buffer;<br/>}<br/></pre><br/><br/>The <code>TIEHANDLE</code> function receives and holds a scalar value as the content of the standard input when it's associated with <code>STDIN</code>. For <code>READ</code>, the second parameter (<code>$_[1]</code>) is a reference (not a Perl "reference", but in the broader computer science terminology) to the receiving variable, since Perl subroutines are call-by-reference.<br/><br/>From here on the main class EmbeddedPerlSandbox is defined.<br/><br/><pre class="brush: cpp"><br/>package EmbeddedPerlSandbox;<br/><br/>$SANDBOX_OUTPUT = "";<br/><br/>%CODE_STORE = {};<br/><br/>BEGIN<br/>{<br/> push @INC, Win32::GetCwd() . "\\perl_lib";<br/><br/> *CORE::GLOBAL::exit = \&amp;EmbeddedPerlSandbox::exit;<br/> *CORE::GLOBAL::flock = \&amp;EmbeddedPerlSandbox::flock;<br/>}<br/></pre><br/><br/><code>$sandbox_output</code> is the global package variable that holds the standard output. The C++ part retrieves the output of a Perl user script by referencing this variable later. <code>%CODE_STORE</code> is a cache that holds a compiled Perl script with its unique package name as a key. The <code>BEGIN</code> block gets evaluated when the C++ code loads this setup script. <code>@INC</code> is the array of library paths. The "perl_lib" directory under the current directory is added to it. To obtain the current directory it uses <code>Win32::GetCwd</code> which is a function in a builtin class for the Windows version of Perl without loading the <code>Cwd</code> module. Also it overrides the <code>exit</code> and the <code>flock</code> builtin functions by assigning subroutine references to their type globs. The <code>exit</code> function ends the Perl interpreter by calling <code>exit()</code> in C, which means not only the Perl interpreter but also the entire application just ends its execution if it's called. Such a situation must be avoided for an obvious reason. The <code>flock</code> function should work in theory even in the Windows version, but when I tried it it stopped execution there due to an unknown reason. As I explained already the C++ section is synchronized by a critical section, so the Perl section is synchronized too. I decided to trap <code>flock</code> and substitute it with an NOP function.<br/><br/>The next subroutine is necessary to make a user script behave like a Perl CGI. If a persistent interpreter just executes the same compiled code twice, all global variables persist in the second execution. For example,<br/><br/><pre class="brush: cpp"><br/>if (!defined($var))<br/>{<br/> $var = 1;<br/>}<br/><br/>print "$var\n";<br/><br/>$var++;<br/></pre><br/><br/>if this code is executed repeatedly in a persistent interpreter the displayed number continues to increase. This behavior is OK for a Java servlet and other persistent web applications, but not desirable if a Perl CGI is the target of emulation. It can be avoided by clearing global package variables previously defined in the package before executing a user code. This <code>cleanupSymbolTable</code> subroutine does the job by scanning the symbol table hash (aliased as "stash" in the Perl jargon) of the user code package. In the Perl language, the reflection system is readily available in the form of the symbol table and it's almost too exposed if you ask me. Though I don't like it because it can be a source of nasty Perl hacks, the reflection itself is certainly a necessary feature when creating a plugin system or a self-contained world of DSL.<br/><br/><pre class="brush: cpp"><br/>sub cleanupSymbolTable<br/>{<br/> my $id = shift;<br/><br/> while (my ($name, $glob_entry) = each %{$id . "::"})<br/> {<br/> local *v = $glob_entry;<br/> if (defined($v))<br/> {<br/> undef $v;<br/> }<br/><br/> if (defined(@v))<br/> {<br/> undef @v;<br/> }<br/><br/> if (defined(%v))<br/> {<br/> undef %v;<br/> }<br/> }<br/>}<br/></pre><br/><br/><code>$id</code> is the name of the user code package. It enumerates all symbols in that package and receives a typeglob for its symbol (See the "Symbol Tables" in <a href="http://perldoc.perl.org/perlmod.html" target="_blank">perlmod</a>). If there is a variable (scalar/array/hash) defined within that typeglob it's undefined. Note that it tests all value types as there may be a typeglob that has variables of all the 3 types defined.<br/><br/>The next subroutine compiles a user code as the name suggests.<br/><br/><pre class="brush: cpp"><br/>sub compile<br/>{<br/> my ($id, $content, $current_dir) = @_;<br/><br/> if (!length($content))<br/> {<br/> return $CODE_STORE{$id};<br/> }<br/><br/> local $SIG{__WARN__} = \&amp;warn;<br/><br/> # the part undefining global variables must come before $content,<br/> # otherwise $content can't contain 'use strict'<br/> my $sandbox =<< "SBOX";<br/>package $id;<br/>sub __wrapper<br/>{<br/> EmbeddedPerlSandbox::cleanupSymbolTable("$id");<br/> \@ARGV = \@_;<br/> $content ;<br/>}<br/>SBOX<br/><br/> my @tmp = @INC;<br/> push @INC, $current_dir;<br/> eval $sandbox;<br/> @INC = @tmp;<br/><br/> if ($@)<br/> {<br/> my $e = $@;<br/> if ($e !~ /via package/ &amp;&amp; $e =~ /line (\d+)/)<br/> {<br/> my $n = $1;<br/> my $m = $n - 6;<br/> $e =~ s/line $n/line $m/g;<br/> }<br/><br/> $e =~ s/\(eval \d+\) //g;<br/><br/> $SANDBOX_OUTPUT = "Content-Type: text/plain\n\nPerl Compilation Error: " . $e . "\n";<br/> }<br/><br/> return $CODE_STORE{$id} = *{$id . "::__wrapper"}{CODE}; # CODE reference - see perlsub<br/>}<br/></pre><br/><br/><code>$id</code> is the unique package name for this script. It has to be a unique name to avoid a conflict with other scripts compiled later in this interpreter instance. <code>$content</code> is the actual Perl CGI script content. If it's empty it just returns an already compiled code for this unique package name. In the actual implementation in the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> the C++ part checks the file time of a script to see if it has to be reloaded or not, but it can be implemented in Perl too if it's preferable for some reason. Though it may not be necessary, the <code>__WARN__</code> signal is trapped with the <code>warn</code> subroutine to suppress a potential interrupt. The <code>$sandbox</code> here-document is the code that wraps a user code into a unique package that has only a wrapper subroutine code in it. A user code is embedded in this wrapper function and placed after the symbol table cleaner function explained above. In this "here document" you have to add "<code>\</code>" to a variable that should not be evaluated at the time of the definition of <code>$sandbox</code>. <code>$current_dir</code> has the current directory for this script. Since the <code>use</code> function in Perl is evaluated at the compile time you have to add the current directory for this script to <code>@INC</code> and restore it after the evaluation of a user code by <code>eval</code>. If a script has an error, <code>$@</code> has the error information. When evaluation is done, the reference to a compiled code can be obtained by getting the "CODE" entry in the subroutine's typeglob.<br/><br/>These subroutines are just utility functions. <code>clearCodeStore</code> is a function that clears the code store as the name suggests. When you run a persistent interpreter you can call this function to reclaim unused resources. <code>addRuntimeError</code> is an error reporting function in executing a user code.<br/><br/><pre class="brush: cpp"><br/>sub clearCodeStore<br/>{<br/> %CODE_STORE = ();<br/>}<br/><br/>sub addRuntimeError<br/>{<br/> my $e = shift;<br/> if (length($SANDBOX_OUTPUT))<br/> {<br/> if ($SANDBOX_OUTPUT !~ /Perl Runtime Error/)<br/> {<br/> $SANDBOX_OUTPUT .= "Perl Runtime Error: " . $e . "\n";<br/> }<br/> }<br/> else<br/> {<br/> $SANDBOX_OUTPUT = "Content-Type: text/plain\n\nPerl Runtime Error: " . $e . "\n";<br/> }<br/>}<br/></pre><br/><br/>The <code>execute</code> subroutine executes a compiled user code.<br/><br/><pre class="brush: cpp"><br/>sub execute<br/>{<br/> my ($id, $compiled, $stdin, $env, $current_dir, $clear_codestore) = @_;<br/><br/> local $SIG{__WARN__} = \&amp;warn;<br/><br/> if (!chdir($current_dir))<br/> {<br/> addRuntimeError("chdir: " . $current_dir);<br/> return;<br/> }<br/><br/> my $oh = tie(*STDOUT, "EmbeddedPerlSandboxOut");<br/> my $ih = tie(*STDIN, "EmbeddedPerlSandboxIn", $stdin);<br/><br/> my %env_vars = ();<br/> foreach my $kv (split(/\n/, $env))<br/> {<br/> if ($kv =~ /([^=]+)=(.+)/)<br/> {<br/> $env_vars{$1} = $2;<br/> }<br/> }<br/><br/> %main::ENV = %env_vars;<br/><br/> my @tmp = @INC;<br/> push @INC, $current_dir;<br/> eval {$compiled->();}; # you need this semicolon...<br/> @INC = @tmp;<br/><br/> if ($@)<br/> {<br/> unless ($@ =~ /EmbeddedPerlSandbox::exit/)<br/> {<br/> my $e = $@;<br/> if ($e !~ /via package/ &amp;&amp; $e =~ /line (\d+)/)<br/> {<br/> my $n = $1;<br/> my $m = $n - 6;<br/> $e =~ s/line $n/line $m/g;<br/> }<br/><br/> $e =~ s/\(eval \d+\) //g;<br/><br/> addRuntimeError($e);<br/> }<br/> }<br/><br/> undef $ih;<br/> untie *STDIN;<br/><br/> if (!$@)<br/> {<br/> $SANDBOX_OUTPUT = <STDOUT>;<br/> }<br/><br/> undef $oh;<br/> untie *STDOUT;<br/><br/> if ($clear_codestore)<br/> {<br/> clearCodeStore();<br/> }<br/>}<br/> </pre><br/><br/>First it changes the current directory to the directory of the user script by calling <code>chdir</code>. I tried to move the current directory in the C++ side by the <code>SetCurrentDirectory</code> Windows API and the <code>PerlDir_chdir</code> Perl C API, but for some reason the Perl side was not affected by them. Then it overrides the standard input and the standard output by using <code>tie</code> and the <code>EmbeddedPerlSandboxIn</code>/<code>EmbeddedPerlSandboxOut</code> classes as explained already. Unfortunately it seems not to be able to override the implicit <code>STDIN</code> expression in <code><></code>. <code>$stdin</code> passes the content of the standard input which is the input by a web user in the case of a web server. <code>$env</code> has environmental variables for CGI. As it's passed in a scalar value delimited by '\n' and '=' it's parsed and stored in the <code>%main::ENV</code> hash to which a CGI code refers. <code>$compiled</code> is the code reference and it's invoked by <code>$compiled->()</code> subroutine expression in the <code>eval</code> block. After it's done the content of the standard output is saved in <code>$SANDBOX_OUTPUT</code> then the standard input and the standard output are restored.<br/><br/>The last section is for the hook functions for these dangerous global functions.<br/><br/><pre class="brush: cpp"><br/>sub exit<br/>{<br/> $SANDBOX_OUTPUT = <STDOUT>;<br/> die("EmbeddedPerlSandbox::exit");<br/>}<br/><br/>sub flock<br/>{<br/> # does nothing<br/> return 1;<br/>}<br/><br/>sub warn<br/>{<br/> die("Perl Warning: " . $_[0] . "\n");<br/>}<br/><br/>1;<br/></pre><br/><br/>The <code>exit</code> function saves the content of the standard output in <code>$SANDBOX_OUTPUT</code> and stops execution by the <code>die</code> function. As the <code>die</code> function can be trapped by <code>eval</code>, the <code>exit</code> function can be emulated this way. <code>flock</code> does nothing and <code>warn</code> is almost the same with <code>exit</code>. A Perl module has to return 1 at the end.<br/><br/>That's all for the Perl side (<code>EmbeddedPerlSandbox.pl</code>). Let's check out the C++ sample code that drives a Perl interpreter. To build it you have to add the Perl source code directory to the include directories of your project and then add the <code>perl59.lib</code> static library to the linker input. The resultant executable requires <code>perl59.dll</code> to run.<br/><br/><pre class="brush: cpp"><br/>#include <string><br/>#include <cstdio><br/><br/>using namespace std;<br/><br/>#include <EXTERN.h><br/>#include <perl.h><br/><br/>EXTERN_C void xs_init (pTHX);<br/>EXTERN_C void boot_DynaLoader (pTHX_ CV* cv);<br/><br/>EXTERN_C void xs_init(pTHX)<br/>{<br/> char *file = __FILE__;<br/> dXSUB_SYS;<br/><br/> /* DynaLoader is a special case */<br/> newXS("DynaLoader::boot_DynaLoader", boot_DynaLoader, file);<br/>}<br/></pre><br/><br/>The <code>xs_init</code> glue code is picked up from the skeleton produced by the <code>ExtUtils::Embed</code> tool (See "Using Perl modules, which themselves use C libraries, from your C program" in <a href="http://perldoc.perl.org/perlembed.html" target="_blank">perlembed</a>).<br/><br/><pre class="brush: cpp"><br/>int _tmain(int argc, _TCHAR* argv[])<br/>{<br/> int argc_perl = 0;<br/> char* embedding[] = {"", "E:\\program\\src\\perlembedtest2\\EmbeddedPerlSandbox.pl"};<br/><br/> PERL_SYS_INIT(&amp;argc_perl, (char***)embedding);<br/><br/> PerlInterpreter* my_perl = perl_alloc();<br/> perl_construct(my_perl);<br/><br/> PL_exit_flags |= PERL_EXIT_DESTRUCT_END;<br/><br/> perl_parse(my_perl, xs_init, 2, embedding, NULL);<br/> perl_run(my_perl);<br/></pre><br/><br/>The <code>embedding</code> char pointer array is an argument list passed to the Perl interpreter. In the second parameter it sets the path of the <code>EmbeddedPerlSandbox.pl</code>. Then it creates and runs a Perl interpreter.<br/><br/>The next part calls the <code>EmbeddedPerlSandbox::compile</code> subroutine in the Perl side to compile a Perl script. The unique package name for this script is in <code>id</code> and the script content is in <code>content</code> for the sake of this sample code.<br/><br/><pre class="brush: cpp"><br/> const char* id = "test";<br/> string content(<br/> "my $data = ''; read(STDIN, $data, 2); print \"$data\n\"; print getc(STDIN) . \"\n\";"<br/> "while (<STDIN>) {print ($i++ . ': ' . $_);}"<br/> "open FH, '>log.txt';print FH 'test'; close(FH);"<br/> );<br/><br/> dSP;<br/><br/> ENTER;<br/> SAVETMPS;<br/> PUSHMARK(SP);<br/><br/> XPUSHs(sv_2mortal(newSVpv(id, 0)));<br/> // don't use 0 as the second argument since Perl uses strlen for 0<br/> XPUSHs(sv_2mortal(newSVpv(content.c_str(), content.size())));<br/><br/> PUTBACK;<br/><br/> call_pv("EmbeddedPerlSandbox::compile", G_EVAL);<br/><br/> SPAGAIN;<br/><br/> SV* sandbox = 0;<br/> if (SvTRUE(ERRSV))<br/> {<br/> POPs; // see perlcall for G_EVAL<br/><br/> printf("ERRSV\n");<br/> printf(SvPVX(ERRSV));<br/> return 0;<br/> }<br/> else<br/> {<br/> sandbox = newSVsv(POPs);<br/> }<br/><br/> PUTBACK;<br/> FREETMPS;<br/> LEAVE;<br/></pre><br/><br/>It pushs two strings <code>id</code> and <code>content</code> in a locally copied Perl stack. The <code>newSVpv</code> function creates a new Perl scalar value for a string (pv: pointer value). If you set 0 for the second parameter it uses C <code>strlen()</code> to calculate the length of the pointed string by a <code>char*</code> pointer. Since a Perl script can contain a null character (though unlikely) this sample gives the length of the script explicitly by using C++ <code>basic_string</code> for <code>content</code>. When the parameters are ready, it puts the stack back to Perl by the <code>PUTBACK</code> macro and calls the subroutine by <code>call_pv</code>. See the details for Perl stack manipulation in <a href="http://perldoc.perl.org/perlcall.html" target="_blank">perlcall</a>. If <code>call_pv</code> is successful it pops the return value from the stack and assigns it to a new Perl scalar value <code>sandbox</code> that holds a compiled Perl code reference.<br/><br/>In this section it actually execute the compiled Perl code. <code>in</code> is a C++ <code>basic_string</code> object that has an emulated standard input. <code>env</code> is supposed to have environmental variables, but this sample code omits it. <code>new_dir</code> is the current directory for this Perl code. As the sample Perl script fiddles with a file, it is created in this directory. Though the <code>EmbeddedPerlSandbox::execute</code> in Perl takes 6 parameters, the 6th parameter is omitted again for this sample. It calls <code>EmbeddedPerlSandbox::execute</code> with 5 arguments with no return value expected (<code>G_VOID</code>).<br/><br/><pre class="brush: cpp"><br/> string in("abcHello world\nI think\nTherefore I am\n");<br/><br/> string env;<br/><br/> string new_dir("E:\\program\\src\\perlembedtest2\\Debug");<br/><br/> ENTER;<br/> SAVETMPS;<br/> PUSHMARK(SP);<br/><br/> XPUSHs(sv_2mortal(newSVpv(id, 0)));<br/> XPUSHs(sandbox);<br/> XPUSHs(sv_2mortal(newSVpv(in.c_str(), in.size())));<br/> XPUSHs(sv_2mortal(newSVpv(env.c_str(), env.size())));<br/> XPUSHs(sv_2mortal(newSVpv(new_dir.c_str(), new_dir.size())));<br/><br/> PUTBACK;<br/><br/> call_pv("EmbeddedPerlSandbox::execute", G_VOID);<br/><br/> SPAGAIN;<br/><br/> STRLEN n_a;<br/> const char* output = SvPV(get_sv("EmbeddedPerlSandbox::SANDBOX_OUTPUT", FALSE), n_a);<br/> int len = (int)n_a;<br/> printf("returned: %d bytes\n", len);<br/><br/> // it can contain a null character<br/> for (int i = 0; i < len; ++i)<br/> {<br/> if (i == 0)<br/> printf("[%c", output[i]);<br/> else<br/> printf("%c", output[i]);<br/> }<br/> if (len != 0)<br/> printf("]\n");<br/><br/> PUTBACK;<br/> FREETMPS;<br/> LEAVE;<br/></pre><br/><br/>After <code>EmbeddedPerlSandbox::execute</code> is executed, it retrieves the Perl scalar value of <code>EmbeddedPerlSandbox::SANDBOX_OUTPUT</code> by calling <code>get_sv</code> and converting it into a <code>char*</code> by <code>SvPV</code>. <code>output</code> is the string that holds the standard output of the Perl script.<br/><br/><pre class="brush: cpp"><br/> PL_perl_destruct_level = 0;<br/><br/> perl_destruct(my_perl);<br/> perl_free(my_perl);<br/> PERL_SYS_TERM();<br/><br/> return 0;<br/>}<br/></pre><br/><br/>When the work is done it frees the Perl interpreter. It's the end of the C++ sample code. I implemented it in the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a> with additional care for a persistent interpreter.<br/><br/>The winner of this round is Perl. But it's not free from a problem, for example its header file redefines common API names with C macros. It's not designed to host multiple interpreters and plagued with excessive C macro use. I hope this mess is cleaned up in <a href="http://dev.perl.org/perl6/" target="_blank">Perl 6</a> with a new VM (<a href="http://www.parrotcode.org/" target="_blank">Parrot</a>), just like Ruby's new VM (<a href="http://www.atdot.net/yarv/" target="_blank">YARV</a>) plans. If <a href="http://dev.perl.org/perl6/" target="_blank">Perl 6</a> becomes the mainstream this article has to be rewritten since the embedding method should be fairly different from the current one, though I somehow doubt the likelihood that Perl 6 becomes too popular. One thing for sure is I'm satisfied with the relative robustness of Perl, for now.久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-22577762524637139802006-11-12T17:17:00.000+09:002015-02-26T02:44:09.247+09:00サーバプッシュの現在 - AJAXの辺縁プッシュテクノロジと呼ばれる、Web通信をデータ放送に見立てた一連のWeb技術が1990年代後半に注目され、Webブラウザによる実装も試みられたことがあった。通常のHTTP接続はWebブラウザがリクエストをサーバに送りつけるとレスポンスを1つしか返さないのに対し、Content-type: multipart/x-mixed-replaceを利用したHTTP上でのサーバプッシュでは、クライアントのリクエスト無しで第二第三のメッセージが連続してサーバから送られてくるように見える。これを利用するとユーザの介入無く動的なページの表示が可能だった。ところがこの方法はNetscapeブラウザしか対応しておらず、Internet Explorerの台頭と共に完全に死滅することになった。<br/><br/>OSの最大シェアを握るMicrosoftの影響力はやはり強く、何年もアップデートされないIEがWebの進化を停滞させているという批判の一方で、画面遷移無しでWebページの内容を更新する手法としてのAJAXの基盤であるXMLHTTPRequestは、Microsoftが生み出したデファクトスタンダードとしてすんなりと世間に受け入れられた(ただしGoogleがその成果を非Microsoft化してしまった)。しかし、XMLHTTPRequestはクライアントがリクエストを一々発行するクライアントプル技術であり、サーバプッシュの真の代用にはなり得ない。<br/><br/>そもそもHTMLやHTTPというWebの標準がここまで貧しい設計でなければ特定ブラウザで動作するしないといった些細な事柄について無意味な議論を重ねる必要は生じなかったはずだ。HTMLベース技術の大半がプラットフォーム間の互換性の維持に労力を費やしている状況はある観点から見れば極めて寒々しい。例えばblogのデファクトスタンダードであるトラックバックという仕組みは、技術的なメリットではなくそれが標準として流布しているという政治的状況によって生かされているのである。その現状を一旦忘れ、例えば全てのWebページが別個のソフトウェアアプリケーションだったらどうだろうか。HTMLがチューリング完全なプログラミング言語で、自己が利用できるサーバまたはクライアント上の資源を理解しつつ全てのWebページが位置透過的・自律的にサービスを提供していたら?<br/><a name='more'></a><br/>マイクロソフトのActiveX(現在は.NET)やSunのJavaは、ネットワーク上に分散配置されたソフトウェアコンポーネント群からなる世界の端緒を開くことを期待された技術だった。ところが様々な政治的要因によりそれらの技術がWebを席巻することは最早あり得ない状況となっている。ブラウザの備えるソフトウェアスタックのサポートはECMAScriptとW3C DOM/CSSのサポートに留まり、WebページそのものはXMLに見られるようにますますデータ化の傾向を強めるかに見える。その傾向と一線を画するのがActiveXコンテナに収まってIE上で動作するAdobe Flashである。2001年にリリースされたWindows XPをインストールして最初に表示されるプロダクトツアーはMacromedia Flashで駆動されていた。Flashオブジェクトを作成するためのActionScriptは現在3.0までバージョンアップし、Java並のプログラマビリティを備える。実装のパフォーマンスもJavaアプレットと比較して高く、Webアプリケーションのユーザビリティを阻害するストレスがほとんどない。絶対的パフォーマンスを除けば、ActiveXコンポーネントに劣るのはデコンパイルが容易な点くらいだろう。Flex 2 SDKは無料化され、誰でも無料でFlashアプリケーションを開発できる。先日のMozilla FoundationへのActionScript VM寄贈も記憶に新しい。その上サーバサイドActionScriptがAdobe製品以外にも拡がることにでもなれば、FlashがWebの覇者となる未来も決して遠くないだろう。あるいは現在既にそうなっているかもしれない。<br/><br/>しかしFlashとてWebの基幹技術のHTMLと融合したわけではなく、両者の間の溝は厳然として存在する。Webブラウザのプラグインとして実装されているFlashその他の技術は、HTMLベースの技術と異なり、WebブラウザのUI上では異物感を感じさせることが少なくない。ブラウザ自身の提供するUIウィジェットの中でプラグインの表示領域のみが浮く、表示フォントの違い、右クリック時のコンテクストメニューの違い、Javaアプレットの場合は起動自体の遅さなど、差異については枚挙にいとまがない。そもそもコンテンツホルダーの要求でテクストのコピーが禁止になっているなど、風通 しの悪さも感じさせられることがあるかもしれない。これも過渡期の現象で遠い将来にはFlashが全てのUIを覆い尽くすことになるのだろうか。Flashがプロプライエタリな技術に留まる限りこの展望は全く荒唐無稽といえる。AJAXの定義をJavaScriptによるWebブラウザのUI操作全般に拡張する動きに見られるように、ECMAScriptのスタックがツールも含めて十分に成長してくれば、WebのUIにダイナミックで柔軟なプログラマビリティを与える道具としてのFlashは存在意義を失ってしまう。そして、歴史をひもとけばFlashのプログラミング言語ActionScript自体がECMAScriptから派生している(ただし2.0でクラスライブラリ階層や強い型付けが導入されたため現在のActionScriptはプロトタイプベースではない)。つまり、現時点では、Flashを擁するAdobe社が、Microsoftとその他の対立に乗じていわば「漁夫の利」を得た状況となっているわけだ。そして、この不安定な均衡に満足しないAdobeは既に先手を打ち、将来にわたりFlashの支配を保証するために、オープンな<a href="http://www.mozilla.org/projects/tamarin/" target="_blank">Tamarin Project</a>に打って出た。<br/><br/>この現状を認識した上で冒頭のサーバプッシュの話に戻ると、かつてのサーバプッシュに現在最も近い技術はWeb上のストリーミング映像サイトで目にすることが出来る。ただし、これらは実際はサーバプッシュではなくクライアントプルでプロトコル上実装されていることもあるのでストリーミン グ即サーバプッシュを意味するわけではない。ここでもプラットフォーム中立性やコンテンツホルダの要請により、QuicktimeやWindows Media PlayerオブジェクトのWebページへの埋め込みではなく、FlashによるFlash Videoフォーマットのストリーミングが一般的になりつつある。HTTPではない(あるいはHTTPトンネリングされた)プロトコルを喋るストリーミン グサーバとWebブラウザの間をFlashが取り持っている。Webブラウザがそれ自身でリッチなアニメーションや動画を処理する標準方式が存在しない以上、先に述べたUI上でのFlashの異物感もやむを得ないと言えるし、このストリーミングは基本的にFlash内で完結している。しかし、イメージ/バイナリベースではない、テクストベースのサーバプッシュについてはどうか。<br/><br/><a href="../../dice/index_j.html">DICE</a>のWebサーバ 部分にIRCチャンネルのWebインターフェイス機能を画面遷移無しで持たせようと考えたとき、真っ先に頭をよぎったのはNetscapeブラウザが持っていたサーバプッシュ機能だった。もちろん、Internet Explorerでは対応していないこの方式は使用することはできない。<a href="../../dice/index_j.html">DICE</a>は1つのサーバでIRCインターフェイスとWebインターフェイスの両方を別々のポートから同時に提供するのでJavaアプレットやFlashのセキュリティサンドボックスの制限をクリアできるとはいえ、それらプラグインでIRCクライアントのUIを作った場合のUIの異物感、不統一感は避けたい。逆に、HTML寄りの方法としてAJAXを使う場合、新しいメッセージがチャンネルに存在していないか調べるためにサーバに対し一定時間毎にXMLHTTPRequestで接続を張ってテストするというポーリングを行わなければならない。これはスマートではないし、サーバへの負荷が高い。AJAXの最悪の使用法だ。<br/><br/>あるいは、サーバプッシュを真似る古典的な手法として、HTTPの接続を切らずに隠しIFRAMEやXMLHTTPRequestによる接続先にサーバからJavaScriptの文を一つ一つ送らせ、フレームやXMLHttpRequestのプロパティといったローカル受信バッファへのポーリングを一定時間毎に行うことにより、サーバ自体へのポーリングを回避しつつ動的なページの更新をエミュレートするというものがある。AJAXに飽き足らないユーザの中にはこの手法をCometあるいはHTTPストリーミングなどと呼んでAJAXのパターンに含めようという動きもあり、例えば<a href="http://www.pushlets.com/" target="_blank">Pushlet</a>はそれを系統的にまとめ上げたツールキットである。しかしこの方法はいわゆるハックの部類に属し、ブラウザの読み込み表示が表示されたままになるという問題の他、クライアント側のタイムアウト時間、バッファリングや同時接続数制限といった仕様に依存する。実装上も、CGIなどのWebアプリケーションとして長時間スレッドをsleepさせるようなものはパフォーマンスの観点から論外である上に、<a href="../../dice/index_j.html">DICE</a>自体に実装するにも煩雑すぎる。<br/><br/>IRCチャンネルにWebインターフェイスを設ける際の注意点として、上記のようなクライアントやサーバ側の仕様上の問題に加え、IRCチャンネルのセキュリティの問題がある。従来は<a href="../../dice/index_j.html">DICE</a>のWebインターフェイスからIRCチャンネルへ発言を投稿するには、サーバがJavaScriptとスタイルシートで文字列(機械的な読み取りが困難なよ うにノイズを加えてある)を描き、投稿者にそれを一々読ませて投稿時に記入させ、HTTPのPOSTで投稿させていた。故意の連続投稿やスパムを未然に防止するためである。しかしWeb掲示板ならともかくリアルタイム性が売りのIRCにこの形式はそぐわない。HTTPストリーミングと組み合わせた場合には輪をかけて非効率である。当然実装も複雑になる。IRC側ではプロクシサーバのチェックまで行っているにも関わらず、Web側のユーザの参加資格には何の制約もないのも問題だ。反面、IRCチャンネル内でWebのユーザをアクセス禁止にすることは容易ではない。<br/><br/>そこで、Flashをソケット通信のバックエンドとしてのみ用いてブラウザをIRCサービスへ直接接続させ、UIはすべてJavaScriptによるDOM操作で構築することを考えた。ページに不可視のFlashオブジェクトを埋め込み、FlashはソケットでIRCサービスのポートへ接続し、通常のIRCクライアント同様に振る舞う。ただしFlashは出来る限りネットワークプロクシの役割に留め、IRCクライアントとしてのロジックはJavaScript側で実装する。この方法ならばプラットフォーム間の差異を最小に留めつつIRCチャンネルのWebインターフェイス を提供でき、セキュリティ上・実装上のオーバーヘッドも低い。テクストベースのWebインターフェイスにFlashプラグインを接ぎ木することによるUI上 の拒絶反応も当然無い。サーバにIRCクライアントとして接続する以前に取得する必要のある動的な情報(例えばIRCのポート)のみAJAXで取得すればよい。UIはスクリプト言語のJavaScriptで制御されているのでFlashと異なりコンパイル無しでサーバ管理者が自由に編集でき、アプリケーションの配布には都合がよいという利点もある。<br/><br/>早速コードを見てみよう。以下、<a href="../../dice/index_j.html">DICE</a>に 添付しているIRCクライアントWebアプリケーションのAdobe Flash 9向けFlashオブジェクトProxyIRCのActionScript 3.0ソースコードProxyIRC.asを解説していく。最初IRCProxyという名称で作成していたが何故かエラーが発生したのでProxyIRC という名称に変更した。ビルドには、最近無償配布が開始されたコマンドライン開発ツール<a href="http://www.adobe.com/jp/products/flex/sdk/" target="_blank">Adobe Flex SDK</a>を使用する。 コンパイル時のコマンドラインオプションは <code>mxmlc -default-size 1 1 -default-frame-rate=30 -default-background-color=0xFFFFFF ProxyIRC.as</code> とでもすればよいだろう。<br/><br/><pre class="brush: cpp"><br/>package<br/>{<br/> import flash.display.*;<br/> import flash.external.*;<br/> import flash.net.*;<br/> import flash.events.*;<br/> import flash.utils.*;<br/><br/> // class name may conflict with the JavaScript engine for IE<br/> // when ExternalInterface is in use, be careful to test different names<br/> public class ProxyIRC extends Sprite<br/> {<br/> private var socket:Socket;<br/> private var receivedMessageBuffer:String;<br/> private var isJIS:Boolean;<br/><br/> public function ProxyIRC()<br/> {<br/> socket = new Socket();<br/> configureListeners(socket);<br/> isJIS = false;<br/><br/> receivedMessageBuffer = new String();<br/><br/> // It seems the JS engine for IE has a serious problem with the functionName argument.<br/> // Don't use an existing JS function name.<br/> ExternalInterface.addCallback("testLoopback", loopbackToJavaScript);<br/> ExternalInterface.addCallback("connectIRC", connect);<br/> ExternalInterface.addCallback("sendIRC", send);<br/> ExternalInterface.addCallback("setJIS", setCharacterEncoding);<br/> ExternalInterface.addCallback("quitIRC", disconnect);<br/> }<br/></pre><br/><br/>ActionScript 3.0は前述のようにJavaScript風からJava風に進化したような言語なので、Javaを知っているならばほとんど言語仕様を見なくてもライブラリリファレンスから必要な道具を拾うだけで直ちにプログラミングを開始できるだろう。型が変数名の後に付くとか、XMLをソースコード内に直接記入できるとかという程度の違いしかない。このクラス<code>ProxyIRC</code>はFlashのディスプレイリストの最も単純な基本クラス<code>Sprite</code>を継承し、3つのプロパティを保持している。<code>socket</code>は文字通り接続対象に接続するためのTCP/IPソケットであり、<code>receivedMessageBuffer</code>は送られてきたデータを解析するために溜めておくバッファである。<code>isJIS</code>は、日本語で行われるIRCのエンコーディングは通常JISのため、内部の文字列を全てutf-8で扱うJavaScriptとやりとりするときに日本語向けの相互変換を行うか否かのユーザ設定を保持するためのフラグである。コンストラクタ内での一連のExternalInterface.addCallbackの呼び出しは、JavaScriptの関数名とFlashのメソッドとの関連付けを登録し、JavaScriptからFlashの関数名を呼び出せるようにするための準備である。<br/><br/><pre class="brush: cpp"><br/> public function loopbackToJavaScript():void<br/> {<br/> ExternalInterface.call("loopbackFromFlash");<br/> }<br/><br/> public function setCharacterEncoding(jis:Boolean):void<br/> {<br/> isJIS = jis;<br/> }<br/><br/> public function connect(host:String, port:uint):void<br/> {<br/> if (socket.connected)<br/> {<br/> socket.writeUTFBytes("QUIT\n");<br/> socket.flush();<br/> socket.close();<br/> }<br/><br/> socket.connect(host, port);<br/> }<br/><br/> public function disconnect():void<br/> {<br/> if (!socket.connected)<br/> return;<br/><br/> socket.writeUTFBytes("QUIT\n");<br/> socket.flush();<br/> socket.close();<br/> }<br/></pre><br/><br/><code>loopbackToJavaScript</code>はFlashが正常に動作しているかJavaScript側から調べるためのメソッドである。JavaScript側から呼び出されると逆にJavaScript側の<code>loopbackFromFlash</code>関数を実行する。<code>connect</code>は サーバへの接続を実行するための関数だが、IRCクライアントUIの実装上、接続中は接続開始のボタンが切断ボタンになっているので、接続中に呼び出されると接続を直ちに切断するようにしてある。この部分はJavaScriptのUIのことを考慮せず分離すべきだったかも知れない。<code>disconnect</code>メソッドは単純にサーバから切断するためのメソッドである。ちなみにIRCはテクストベースのプロトコルで一つ一つのメッセージは'\n'で区切られる。QUITは最も単純なメッセージである。プロトコルの詳細はRFC 2812を参照して頂きたい。<br/><br/><pre class="brush: cpp"><br/> private function configureListeners(dispatcher:IEventDispatcher):void<br/> {<br/> dispatcher.addEventListener(Event.CLOSE, closeHandler);<br/> dispatcher.addEventListener(Event.CONNECT, connectHandler);<br/> dispatcher.addEventListener(ProgressEvent.SOCKET_DATA, dataHandler);<br/> dispatcher.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);<br/> dispatcher.addEventListener(ProgressEvent.PROGRESS, progressHandler);<br/> dispatcher.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);<br/> }<br/><br/> public function send(utf8string:String):void<br/> {<br/> if (isJIS)<br/> {<br/> var b:ByteArray = new ByteArray();<br/> b.writeMultiByte(utf8string + "\n", "iso-2022-jp");<br/> socket.writeBytes(b, b.bytesAvailable);<br/> }<br/> else<br/> socket.writeUTFBytes(utf8string + "\n");<br/> socket.flush();<br/> }<br/><br/> private function closeHandler(event:Event):void<br/> {<br/> ExternalInterface.call("onClose");<br/> }<br/><br/> private function connectHandler(event:Event):void<br/> {<br/> ExternalInterface.call("onConnect");<br/> }<br/></pre><br/><br/><code>configureListeners</code>メソッドは、引数で指定されたオブジェクトに起こったイベントを監視するためのイベントリスナをまとめて登録するための初期化メソッドで、既に見たように<code>ProxyIRC</code>のコンストラクタで呼ばれる。引数はソケットオブジェクトなので、ソケットが切断される、ソケットがデータを受信する等のイベントに対してそれぞれの処理メソッドの参照を関連付けている。<code>send</code>はサーバに対して文字列を送信するためのメソッドで、JavaScript側からutf-8で送られてきた文字列を、JISエンコーディングが有効になっている場合は<code>ByteArray</code>クラスの<code>writeMultibyte</code>メソッドを利用してJIS(iso-2022-jp)エンコーディングに変換してから送信する。それが必要ない場合はそのままutf-8文字列として送信している。<code>closeHandler</code>、<code>connectHandler</code>はJavaScript側にソケットの切断・接続を通知するための処理メソッドである。<br/><br/><pre class="brush: cpp"><br/> private function dataHandler(event:ProgressEvent):void<br/> {<br/> var buffer:String = new String();<br/> if (isJIS)<br/> buffer = socket.readMultiByte(event.bytesLoaded, "iso-2022-jp");<br/> else<br/> buffer = socket.readUTFBytes(event.bytesLoaded);<br/><br/> var c:String = new String();<br/> for (var i:uint = 0; i < buffer.length; ++i)<br/> {<br/> c = buffer.charAt(i);<br/> if (c == "\n" || c == "\r")<br/> {<br/> if (receivedMessageBuffer.length != 0)<br/> {<br/> if (receivedMessageBuffer.length >= 4<br/> &amp;&amp; receivedMessageBuffer.substring(0, 4).toUpperCase().localeCompare("PING") == 0)<br/> send("PONG");<br/> else<br/> ExternalInterface.call("output", receivedMessageBuffer);<br/> }<br/> receivedMessageBuffer = "";<br/> }<br/> else<br/> receivedMessageBuffer += c;<br/> }<br/> }<br/><br/> private function progressHandler(event:ProgressEvent):void<br/> {<br/> // trace("progressHandler loaded:" + event.bytesLoaded + " total: " + event.bytesTotal);<br/> }<br/><br/> private function ioErrorHandler(event:IOErrorEvent):void<br/> {<br/> ExternalInterface.call("reportError", "I/O Error: " + event);<br/> }<br/><br/> private function securityErrorHandler(event:SecurityErrorEvent):void<br/> {<br/> ExternalInterface.call("reportError", "Security Error: " + event);<br/> }<br/> }<br/>}<br/></pre><br/><br/><code>dataHandler</code>メソッドはやや複雑だが、サーバからデータを受信したときの処理を記述したメソッド である。JISエンコーディングが有効なときはJISとしてソケットからバッファへ読み込み、そうでなければutf-8文字列として読み込む。ループ処理 はバッファから個々のIRCメッセージを切り出すためのもので、メッセージ区切りの改行文字を見つけると一つメッセージを受け取ったと判断してJavaScript側の処理関数<code>output</code>を呼び出している。ただし例外として、<code>PING</code>というIRCメッセージを受け取ると、JavaScript側に通知せず<code>PONG</code>と いうメッセージを送り返す。これはIRCの接続維持メカニズムで、一定時間活動がないと回線が接続されているかどうかのポーリングが行われるというもので ある。JavaScript側へ渡すことも出来るが、無駄なオーバーヘッドとなるのでここではFlash側で適切に処理している。<code>ioErrorHandler</code>はI/Oエラー時の処理メソッド、<code>securityErrorHandler</code>はFlashのセキュリティ違反が起こったときに呼ばれるメソッドである。Javaアプレットと同じく、Flashのセキュリティサンドボックスはソケットの接続をアプレットがホストされているサーバと同じドメインのサーバに制限しているので、それに違反するとソケットにセキュリティエラーが起こる。ただ し、Flashの場合は、接続先外部サーバがルートにXMLのクロスドメインポリシーファイルを持っていればこの制限は回避できるため、外部サーバに接続するよう指示された場合Flashはまずクロスドメインポリシーファイルを探索し、見つからないとセキュリティエラーを発生させる。<br/><br/>ソケット通信と日本語JISエンコーディング変換を行うバックエンドのFlashコードは以上である。今度は、JavaScriptで構築されたユーザインターフェイスを見ていこう。基本的な動作イメージは、よくあるGUIのIRCクライアントのように、チャンネルの内容を表示するバッファとチャンネルのメンバーを表示するコラムがあり、さらに発言を入力したりするための操作パネルが下部にある。さらにチャンネルやその他のタブ画面の切り替えを行 うために、Windowsのタスクバーのようにタブを列挙したバーが操作パネルの下部に付属している。最初から存在する消去できない特殊なタブとして、ヘ ルプファイル表示タブ(help)、生の通信ログを書き出すためのタブ(raw)、IRCサーバーが発したメッセージを表示するためのタブ(status)、チャンネルリスト表示用タブ(channels)がある。これらの要素を、JavaScriptによるDOM操作で、Internet Explorer 6またはFirefox 2以上を対象としたIRCクライアントWebアプリケーションとして駆動する。<br/><br/>IRCクライアント本体のコードを見る前に、AJAXやDOM関係の簡易ユーティリティメソッドをまとめたJavaScriptファイルdice.jsをまず提示しておく。IRCクライアントの方ではここに登場するメソッドを使用している。<br/><br/><pre class="brush: cpp"><br/><br/>var isMozilla = navigator.userAgent.indexOf('Gecko') != -1;<br/>var isIE = window.ActiveXObject;<br/><br/>function createHttpRequest()<br/>{<br/> if (isIE)<br/> {<br/> try<br/> { // CLSID_XMLHTTP<br/> // v 3.0<br/> return new ActiveXObject("Msxml2.XMLHTTP");<br/> }<br/> catch (e)<br/> {<br/> try<br/> {// v 2.x<br/> return new ActiveXObject("Microsoft.XMLHTTP");<br/> }<br/> catch (e2)<br/> {<br/> return null;<br/> }<br/> }<br/> }<br/> else if (window.XMLHttpRequest) // non-IE<br/> {<br/> var hr = new XMLHttpRequest();<br/> if (isMozilla)<br/> hr.overrideMimeType('text/xml');<br/> return hr;<br/> }<br/> else<br/> {<br/> return null;<br/> }<br/>}<br/><br/>function getInnerHTML(id)<br/>{<br/>// if (isIE)<br/>// {<br/> return document.getElementById(id).innerHTML;<br/>// }<br/><br/>/* For old Mozilla compatibility<br/> var html = "";<br/> node = document.getElementById(id);<br/> for (var i = 0; i < node.childNodes.length; ++i)<br/> html += getOuterHTML(node.childNodes.item(i));<br/><br/> return html;*/<br/>}<br/><br/>/* For old Mozilla compatibility<br/>function getOuterHTML(node)<br/>{<br/> var s = "";<br/><br/> switch (node.nodeType)<br/> {<br/> case 1: // ELEMENT_NODE<br/> s += "<" + node.nodeName;<br/> for (var i = 0; i < node.attributes.length; ++i)<br/> {<br/> if (node.attributes.item(i).nodeValue != null)<br/> {<br/> s += " "<br/> s += node.attributes.item(i).nodeName;<br/> s += "=\"";<br/> s += node.attributes.item(i).nodeValue;<br/> s += "\"";<br/> }<br/> }<br/><br/> if (node.childNodes.length == 0<br/> &amp;&amp; (node.nodeName == "IMG"<br/> || node.nodeName == "HR"<br/> || node.nodeName == "BR"<br/> || node.nodeName == "INPUT"<br/> ))<br/> s += ">";<br/> else<br/> {<br/> s += ">";<br/> s += getInnerHTML(node);<br/> s += "<" + node.nodeName + ">"<br/> }<br/> break;<br/> case 3: //TEXT_NODE<br/> s += node.nodeValue;<br/> break;<br/> case 4: // CDATA_SECTION_NODE<br/> s += "<![CDATA[" + node.nodeValue + "]]>";<br/> break;<br/> case 5: // ENTITY_REFERENCE_NODE<br/> s += "&amp;" + node.nodeName + ";"<br/> break;<br/> case 8: // COMMENT_NODE<br/> s += "<!--" + node.nodeValue + "-->"<br/> break;<br/> }<br/><br/> return s;<br/>}<br/>*/<br/><br/>function replaceElement(id, html)<br/>{<br/>// if (isIE)<br/>// {<br/> document.getElementById(id).innerHTML = html;<br/>// }<br/>/* For old Mozilla compatibility<br/> else<br/> {<br/> var e = document.createElement('span');<br/> e.setAttribute('id', 'temp');<br/><br/> var range = document.createRange();<br/> range.selectNodeContents(document.body);<br/> range.collapse(true);<br/> e.appendChild(range.createContextualFragment(html));<br/><br/> var outer = document.getElementById(id);<br/> while (outer.firstChild)<br/> outer.removeChild(outer.firstChild);<br/> outer.appendChild(e);<br/> }*/<br/>}<br/><br/>function appendElement(id, html)<br/>{<br/>// if (isIE)<br/>// {<br/> document.getElementById(id).innerHTML += html;<br/>// }<br/>/* For old Mozilla compatibility<br/> else<br/> {<br/> var e = document.createElement('span');<br/> e.setAttribute('id', 'temp');<br/> var range = document.createRange();<br/> range.selectNodeContents(document.body);<br/> range.collapse(true);<br/> e.appendChild(range.createContextualFragment(html));<br/> document.getElementById(id).appendChild(e);<br/> }*/<br/>}<br/><br/>function sendHTTP(data, method, fileName, callback, async)<br/>{<br/> var hr = createHttpRequest();<br/> hr.open(method, fileName, async);<br/> hr.setRequestHeader("If-Modified-Since", "Thu, 01 Jun 1970 00:00:00 GMT");<br/> hr.onreadystatechange = function()<br/> {<br/> if (hr.readyState == 4)<br/> {<br/> callback(hr);<br/> }<br/> }<br/><br/> hr.send(data)<br/>}<br/><br/>// replace an element in an HTML document with a text node in an AJAX XML response<br/>// that has the same tag name as the ID of the aforementioned HTML element<br/>function replaceElementByAjaxTagName(hr, tagname)<br/>{<br/> var nodelist = hr.responseXML.getElementsByTagName(tagname);<br/> if (nodelist != null)<br/> {<br/> var x = nodelist.length;<br/> for (var i = 0; i < x; ++i)<br/> {<br/> var n = nodelist.item(i);<br/> var nn = n.firstChild;<br/> // NODE_TEXT == 3 || NODE_CDATA_SECTION == 4<br/> while (nn != null &amp;&amp; (nn.nodeType == 3 || nn.nodeType == 4))<br/> {<br/> replaceElement(tagname, nn.nodeValue);<br/> nn = nn.nextSibling;<br/> }<br/> }<br/> }<br/>}<br/><br/>function getAjaxTextNodeByTagName(hr, tagname)<br/>{<br/> var nodelist = hr.responseXML.getElementsByTagName(tagname);<br/> if (nodelist == null)<br/> return null;<br/><br/> return nodelist.item(0).firstChild.nodeValue;<br/><br/> //return nodelist.item(0).textContent; // IE fails<br/>}<br/><br/>function unixtime2localdate(t)<br/>{<br/> var d = new Date;<br/> d.setTime(t * 1000);<br/><br/> return d.toLocaleString();<br/><br/> //var h = String(d.getHours());<br/> //if (h.length == 1)<br/> // h = "0" + h;<br/><br/> //var m = String(d.getMinutes());<br/> //if (m.length == 1)<br/> // m = "0" + h;<br/><br/> //var s = String(d.getSeconds());<br/> //if (s.length == 1)<br/> // s = "0" + h;<br/><br/> //return String(d.getYear() % 1900 + 1900) + "/"<br/> // + String(d.getMonth() + 1) + "/"<br/> // + d.getDate() + " "<br/> // + h + ":"<br/> // + m + ":"<br/> // + s;<br/>}<br/></pre><br/><br/>御覧のように、かなりの部分がコメント化されて消されているのは、Mozillaが<code>innerHTML</code>をサポートしていなかった時代から使っていた互換性維持のためのコードを省略したことによる。<code>innerHTML</code>自体はあくまで非標準だが、今回はあえて古いブラウザは切り捨てておそらく効率的であろう<code>innerHTML</code>を使用するという道を選んだ。<br/><br/>ここから、JavaScriptによるIRCクライアントのコードを含んだHTMLファイルirc_client.htmlを見ていく。<br/><br/><pre class="brush: cpp"><br/><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><br/><html><br/><head><br/><title>Web IRC Client for KLassphere DICE</title><br/><br/><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><br/><meta HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE"><br/><meta HTTP-EQUIV="Expires" CONTENT="Mon, 04 Dec 1999 21:29:02 GMT"><br/><br/><style type="text/css"><!--<br/>td, form, input, button, #tabs<br/>{<br/> font-size: 12px;<br/> font-family: Verdana, Geneva, Arial, sans-serif;<br/>}<br/>--><br/></style><br/></head><br/><script language="JavaScript" src="dice.js"></script><br/><script language="JavaScript"><br/><!--<br/>/*<br/><br/>Flash/Javascript IRC Client for KLassphere DICE - (c) RyuK 2006 - 2007 All Rights Reserved<br/><br/>klassphere[at.mark]gmail.com<br/>http://zzz.zggg.com/<br/>http://aiueo.da.ru/<br/><br/>This file is not redistributable.<br/><br/>*/<br/><br/>var flashIsEnabled = false;<br/>var isConnected = false;<br/><br/>var mapChannelBuffer = new Object();<br/>var mapSpecialBuffer = new Object();<br/>var mapPrivateMessageBuffer = new Object();<br/><br/>var helpBuffer = new ChannelBuffer;<br/>var rawBuffer = new ChannelBuffer("(This is the buffer where raw server outputs are dumped in)<br>");<br/>var statusBuffer = new ChannelBuffer("(This is the buffer where current status is logged)<br>");<br/><br/>var channelListHeader = ("Channel List<br><br>Push the button to load the list from the server after a connection is established.<br>Click a channel name in the list to join in.<br>"<br/> + "<button onClick=\"loadChannelList();\">Refresh the channel list</button><br><br>");<br/>var channelListBuffer = new ChannelBuffer(channelListHeader);<br/><br/>var myNick = "";<br/>var currentTab = "";<br/>var serverName = "";<br/></pre><br/><br/>まず、グローバル変数の定義から行う。<code>mapChannelBuffer</code>は汎用のObjectとして生成されるが、実際には連想配列として使用され、キーはチャンネル名(IRCチャンネル名は全て'#'という接頭辞が付く - 従ってJavaScriptオブジェクトのプロパティ名としては不適当なので、チャンネル名に対応したオブジェクトのアクセスは.#testではなく ["#test"]のように角括弧で行う)、キーに対応するエントリはIRCチャンネルの表示ウィンドウを表すオブジェクトとする予定である。同様に、<code>mapSpecialBuffer</code>は先に述べた特殊なタブの表示ウィンドウを保持する。<code>mapPrivateMessageBuffer</code>は、他のIRCユーザと1対1のプライベートメッセージを交わすための表示画面を保持する、ユーザ名をキーとした連想配列となる。<code>helpBuffer</code>、<code>rawBuffer</code>、<code>statusBuffer</code>、<code>channelListBuffer</code>はそれぞれの特殊タブの実体で、後で定義される<code>ChannelBuffer</code>というクラスのオブジェクトを個々に保持する。<code>ChannelBuffer</code>は文字通りチャンネルの中身を保持するためのオブジェクトで、ここでは一々オブジェクトを特殊化せずにそのまま他の種類のタブにも流用している。<code>myNick</code>はIRCサーバ上での自分のニックネーム、<code>currentTab</code>は現在選択され表示されているタブの名称、<code>serverName</code>は接続先サーバ名である。<br/><br/>さて、ここで説明の便宜上、irc_client.htmlの最下部に置いてあるHTMLデザインの部分を先に出しておく。デザインの概要は、先に基本動作イメージとして述べたとおりである。冒頭にFlashのProxyIRCが配置され、1ピクセルの大きさなのでユーザからは見えない。(尚、フラッシュのファイル名についている"?1"はWebブラウザによるFlashオブジェクトのキャッシュを抑制するためのもので、 irc_client.htmlの冒頭のヘッダにもキャッシュを抑制するためのmetaタグが置いてある。開発中に頻繁にFlashを更新するためにブラ ウザのキャッシュを抑制すべく置いたが、実際に機能する場合としない場合があるようだ。)チャンネル表示ウィンドウとして主に利用される、IDが<code>currentBuffer</code>の 要素内に、helpタブの内容は既に記述されており、アプリケーションは開始時にこの要素のHTMLを読み込んでヘルプファイルとする。また、チャンネル表示ウィンドウの右上にはXボタンがあり、これを押すとそのタブが消去されるのはWindowsなどのGUIの慣例に拠っている。例えばチャンネルのタブの場合、そのチャンネルから離脱するという効果が得られる。<br/><br/><pre class="brush: cpp"><br/><body onLoad="setup();" onkeydown="onKeyDown(event);"><br/><br/><OBJECT classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" id="ProxyIRC" width="1" height="1"<br/>codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0"><br/> <PARAM name="movie" value="ProxyIRC.swf?1"><br/> <PARAM name="quality" value="high"><br/> <PARAM name="bgcolor" value="#ffffff"><br/> <PARAM name="allowScriptAccess" value="sameDomain"><br/> <param name="src" value="ProxyIRC.swf?1"><br/> <EMBED src="ProxyIRC.swf?1" quality="high" bgcolor="#ffffff" width="1" height="1" name="ProxyIRC" align="middle<br/>" play="true" loop="false" quality="high" allowScriptAccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer"><br/></OBJECT><br/><br/><table width="100%" height="85%" border="1" cellpadding="0"><br/><tr><br/> <td width="100%" height="1%" colspan="2" bgcolor="#0066FF"><br/> <table width="100%" border="0" cellspacing="0"><br/> <tr><br/> <td bgcolor="#0066FF"><font color="#ffffff"><b><span id="buffer_title"></span></b></font></td><br/> <td width="1%"><button onClick="onXButtonClick();">X</button></td><br/> </tr><br/> </table><br/> </td><br/></tr><br/> <tr><br/> <td width="85%" height="460" style="vertical-align: top;"><p><span id="currentBuffer"><b>Welcome to the Web IRC Client for the KLassphere DICE</b><br><br/><br><font size=-1><br/>Usage:<br><br/><br><br/>* This client requires <b>Internet Explorer 6</b> or <b>Firefox 1.5</b> or higher, and <b>Adobe Flash 9</b> plug-in to run. Also <b>JavaScript</b><br/> must be enabled in the web browser options.<br><br><br/>* There are tabs at the bottom of this page, "<b>help</b>", "<b>raw</b>", "<b>status</b>", and "<b>channels</b>". Click them to open. The "help"<br/> tab is this page and has the user manual. "raw" is the buffer where raw server outputs are dumped in. "status" is where current status is logged,<br/> such as negotiation with an IRC server. "channels" is for the channel list for the IRC server.<br><br><br/>* Enter your nick name for IRC in the box on the right of "<b>Nick Name</b>". Then press the "<b>Connect</b>" button to connect to the IRC server.<br/> The boxes on the right are for IRC server name and port.<br><br><br/>* After the connection is established, you are transferred to the "status" tab and can see server messages. Then you can retrieve it the channel<br/> list and join channels from the "channels" tab. Alternatively, or when you want to create a new channel, you can specify the channel name directly<br/> in the box on the right of the "Join Channels" button and join in it by pressing the button. A channel name has to start with "#".<br><br><br/>* When you've joined a channel, a new tab is added in the tab bar at the bottom of this page. You can switch channels by clicking tabs.<br><br><br/>* To speak in a channel, write down your message in the "<b>Message</b>" box and press the Enter key or push the "Send" button.<br><br><br/>* To exchange private messages with a channel member, click a user name in the channel member list and type in a message.<br><br><br/>* The "Unicode (UTF-8)" / "Japanese Multibyte (JIS)" radio buttons are used to change the message encoding. Set it to "Unicode (UTF-8)"<br/> if your channel's encoding is Unicode.<br><br><br/>* When you check the "Raw Message" checkbox this client sends the text entered as is to the IRC server. This command is useful to enter<br/> an IRC command which is not supported by this client. Alternatively you can send a message with "/quote " prefix to gain the same effect.<br><br><br/>* If you want to erase the content of the current tab, push the "Clear This Buffer" button.<br><br><br/>* To part from a channel, click the "X" button on the upper right of the tab.<br><br><br/>* If you are done with chat, push the "Disconnect" button to quit.<br><br><br/></font><br/></span></p></td><br/> <td width="15%" style="vertical-align: bottom;"> <p><span id="members"></span></p></td><br/> </tr><br/></table><br/><form onSubmit="return false;"><input type="button" onclick="connect();" id="buttonConnect" value="(initializing)"><br/> <input type="text" id="IRCHost" value=""> : <input type="text" id="IRCPort" value="" size="5"> Nick Name <input type="text" id="nick" value="" size="16"><br/> <input type="button" onclick="joinChannel();" id="buttonJoin" value="Join Channel"><br/> <input type="text" id="channelName" size="32" onFocus="this.value = '';" value="(enter a channel name here)"><br><br/>Message <input type="text" id="message" size="80"><input type="button" onclick="sendMessage();" value="Send"><br><br/><input type="radio" name="charset" value="utf-8" onclick="setCharset(false)"> Unicode (UTF-8)<br/><input type="radio" name="charset" value="JIS" onclick="setCharset(true)" checked> Japanese Multibyte (JIS) <input type="checkbox" id="raw"> Raw Message<br/> <input type="button" onClick="onClearThisBuffer();" id="buttonClearBuffer" value="Clear this buffer"></form><br/><span id="tabs"></span><br/></body></html><br/></pre><br/><br/>再びJavaScriptコードへ戻ろう。初期化関連の関数がまず登場する。<br/><br/><pre class="brush: cpp"><br/>function callProxyIRC()<br/>{<br/> // IE6 doesn't need this method, but FF3 does<br/> if (isIE)<br/> {<br/> return window["ProxyIRC"];<br/> }<br/> else<br/> {<br/> return document["ProxyIRC"];<br/> }<br/>}<br/><br/>function setup()<br/>{<br/> mapSpecialBuffer["help"] = helpBuffer;<br/> helpBuffer.buffer = getInnerHTML("currentBuffer");<br/><br/> mapSpecialBuffer["raw"] = rawBuffer;<br/> mapSpecialBuffer["status"] = statusBuffer;<br/> mapSpecialBuffer["channels"] = channelListBuffer;<br/><br/> var date = new Date;<br/> document.getElementById("nick").value = ("guest" + date.getTime().toString().slice(-5));<br/><br/> renderTabs();<br/> switchTab("help");<br/><br/> sendHTTP( '' , 'GET', './irc_address', fillServerAddress, true);<br/><br/> // For Firefox 2 and lower, the initialization of Flash is so slow<br/> // that the setup function must be delayed.<br/> setTimeout('setupFlash()', 1000);<br/>}<br/><br/>function setupFlash()<br/>{<br/> callProxyIRC().testLoopback();<br/><br/> if (!flashIsEnabled)<br/> setTimeout('setupFlash()', 2000);<br/>}<br/><br/>function loopbackFromFlash()<br/>{<br/> flashIsEnabled = true;<br/> document.getElementById("buttonConnect").value = "Connect";<br/>}<br/><br/>function fillServerAddress(hr)<br/>{<br/> document.getElementById("IRCHost").value = getAjaxTextNodeByTagName(hr, "IRCHost");<br/> document.getElementById("IRCPort").value = getAjaxTextNodeByTagName(hr, "IRCPort");<br/>}<br/></pre><br/><br/><code>callProxyIRC</code>はFlashのProxyIRCオブジェクトをJavaScriptからの呼び出し対象として取得する関数で、ブラウザ間の扱いの違いを吸収するためのラッパーである。<code>isIE</code>はdice.js内で定義されているフラグで、Internet Explorerに対してtrueとなる。IEの場合は全てのドキュメントオブジェクトはwindowに格納されており、Firefoxの場合はdocumentになる。<code>setup</code>はこのアプリケーションの初期化関数で、HTMLからonLoadで呼び出され、まずそれぞれのタブ群をまとめる連想配列に各々の表示ウィンドウのオブジェ クトを入れる。それから、ユーザのニックネーム欄には"guest"で始まるランダムな文字列が設定される。タブ群の準備が出来たところで<code>renderTabs</code>を呼び出し、タブを列挙したバーの描画を行い、helpタブへタブを切り替える。最後に、Webサーバの<code>/irc_address</code>というファイルを取得するようAJAXで要求を送っている。<a href="../../dice/index_j.html">DICE</a>のWebサービスは<code>/irc_address</code>でIRCサービスのアドレスとポートをXMLファイルとして返すので、<code>fillServerAddress</code>関 数はこの情報を得てフォームの接続先ホスト・ポート欄に記入する。setup関数の最後には、Flash内のtestLoopback関数を呼び出すこと により、Flashがこのブラウザで有効になっているか調べている。Firefox 3やIE6/7ではsetTimeoutで実行を遅らせる必要はないが、Firefox 2だとFlashの初期化がbodyタグでのonloadのタイミングより若干遅れるため、2秒の実行待ち時間を入れている。<br/><br/>続いて、<code>ChannelBuffer</code>クラスの定義を行う。<br/><br/><pre class="brush: cpp"><br/><br/>// ChannelMember class<br/>function ChannelMember(px, n)<br/>{<br/> var obj =<br/> {<br/> prefix : px,<br/> nick : n<br/> };<br/> return obj;<br/>}<br/><br/>// ChannelBuffer class<br/>function ChannelBuffer(buf)<br/>{<br/> if (buf == null)<br/> buf = "";<br/><br/> var obj =<br/> {<br/> buffer : buf,<br/> members : new Array(),<br/><br/> addMember : function(m) // m: ChannelMember object<br/> {<br/> this.members.push(m);<br/> },<br/><br/> // returns the removed channel member object if it's successful<br/> removeMember : function(n) // n: nick<br/> { // don't use splice for compatibility<br/> var ret = false;<br/> var c = new Array();<br/> for (var i in this.members)<br/> {<br/> if (ret)<br/> c.push(this.members[i]);<br/> else if (this.members[i].nick != n)<br/> c.push(this.members[i]);<br/> else<br/> ret = this.members[i];<br/> }<br/><br/> if (ret)<br/> this.members = c;<br/><br/> return ret;<br/> },<br/><br/> sortMembers : function()<br/> {<br/> this.members.sort(this.compareMember);<br/> },<br/><br/> compareMember : function(a, b)<br/> {<br/> if (a.prefix == "@" &amp;&amp; b.prefix != "@")<br/> return -1;<br/> if (a.prefix == "+" &amp;&amp; b.prefix == "")<br/> return -1;<br/> if (a.prefix == "" &amp;&amp; b.prefix != "")<br/> return 1;<br/><br/> if (b.prefix == "@" &amp;&amp; a.prefix != "@")<br/> return 1;<br/> if (b.prefix == "+" &amp;&amp; a.prefix == "")<br/> return 1;<br/> if (b.prefix == "" &amp;&amp; a.prefix != "")<br/> return -1;<br/><br/> //return (a.nick - b.nick); // this doesn't work in IE6<br/><br/> if (b.nick > a.nick)<br/> return -1;<br/> else if (b.nick == a.nick)<br/> return 0;<br/> else<br/> return 1;<br/> },<br/><br/> setMemberMode : function(nick, mode)<br/> {<br/> for (var i in this.members)<br/> {<br/> if (this.members[i].nick == nick)<br/> {<br/> if (mode.charAt(0) == "+")<br/> {<br/> switch (mode.charAt(1))<br/> {<br/> case "o":<br/> this.members[i].prefix = "@";<br/> break;<br/> case "v":<br/> this.members[i].prefix = "+";<br/> break;<br/> }<br/> }<br/> else<br/> this.members[i].prefix = "";<br/><br/> return;<br/> }<br/> }<br/> },<br/><br/> getMemberPrefix : function(nick)<br/> {<br/> for (var i in this.members)<br/> {<br/> if (this.members[i].nick == nick)<br/> {<br/> return this.members[i].prefix;<br/> }<br/> }<br/><br/> return "";<br/> },<br/><br/> // these rendering functions force update when the parameter is omitted.<br/> renderMembers : function(bufferName)<br/> {<br/> var ret = (this.members.length == 0 ? "" : "[members]<br>");<br/><br/> for (var i in this.members)<br/> {<br/> ret += "<a style=\"text-decoration: none;\" href=\"#\" onclick=\"openPrivateMessage('";<br/> ret += (this.members[i].nick + "@" + serverName);<br/> ret += "')\">";<br/> ret += (this.members[i].nick == myNick<br/> ? ("<font color=\"red\">" + this.members[i].prefix + this.members[i].nick + "</font>")<br/> : (this.members[i].prefix + this.members[i].nick));<br/> ret += "</a>";<br/> ret += "<br>";<br/> }<br/><br/> if (!bufferName || currentTab == bufferName)<br/> {<br/> replaceElement("members", ret);<br/> }<br/> },<br/><br/> renderBuffer : function(bufferName)<br/> {<br/> if (!bufferName || currentTab == bufferName)<br/> {<br/> replaceElement("currentBuffer", this.buffer);<br/> }<br/> },<br/><br/> render : function(bufferName)<br/> {<br/> if (!bufferName || currentTab == bufferName)<br/> {<br/> replaceElement("currentBuffer", this.buffer);<br/> this.renderMembers();<br/> }<br/> },<br/><br/> renderTab : function(i)<br/> {<br/> var t = "<span style=\"border: 1px solid gray; padding: 0px 2px 4px 4px; margin: 3px 3px 3px 3px\"><a style=\"text-decoration: none;\" href=\"#\" onclick=\"switchTab('";<br/> t += i;<br/> t += "')\">";<br/> t += i;<br/> t += "</a></span>";<br/> return t;<br/> }<br/> };<br/> return obj;<br/>}<br/></pre><br/><br/>JavaScriptはプロトタイプベースの言語でクラスというものが存在せず、いつでもオブジェクトのプロパティやメソッド、また継 承に使用されるプロトタイプメソッドを付けたり外したりできるので、あるプロパティのセットを持つオブジェクトを返すという、クラスのコンストラクタ風のメソッドを定義することによってオブジェクト指向を模することになる。ここではJSONデータフォーマットの元となったオブジェクト記法でクラスのプロパ ティやメソッドオブジェクトを列挙している。<code>ChannelMember</code>クラスはチャンネル参加メンバーリストの個々のメンバーを表すオブジェクトで、<code>prefix</code>と<code>nick</code>の2つのプロパティを持つ。IRCのチャンネル参加者は階級毎に異なる接頭辞が付くので、その接頭辞とニックネームの双方を別々に保持するための2つのプロパティである。<code>ChannelBuffer</code>クラスは先に出てきたチャンネル表示ウインドウのデータを保持するためのクラスである。<code>buffer</code>はチャンネルのログなどが表示されるメインの領域のデータを保持するためのバッファで、<code>members</code>は<code>ChannelMember</code>の配列を保持するチャンネル参加者リストである。チャンネルメンバー追加、メンバーリストのソートなどの一連のメンバーリスト操作メソッドの後に、メンバー リスト、メイン領域、タブ列挙バーの中の個々のタブの描画を担当するレンダリングメソッドが続く。これらレンダリングメソッドは、対象のタブが現在表示さ れているときのみdice.jsの<code>replaceElement</code>関数を使用してDOMを操作し表示領域を更新する。<br/><br/><pre class="brush: cpp"><br/>function getCurrentTimeString()<br/>{<br/> var ret = "";<br/> var d = new Date();<br/> var tmp = d.getHours();<br/> if (tmp < 10)<br/> tmp = "0" + tmp;<br/> ret += tmp;<br/> ret += ":";<br/> tmp = d.getMinutes();<br/> if (tmp < 10)<br/> tmp = "0" + tmp;<br/> ret += tmp;<br/> ret += ":";<br/> tmp = d.getSeconds();<br/> if (tmp < 10)<br/> tmp = "0" + tmp;<br/> ret += tmp;<br/> return ret;<br/>}<br/><br/>function refreshCurrentBuffer(bufferName, buffer)<br/>{<br/> if (currentTab == bufferName)<br/> replaceElement("currentBuffer", buffer);<br/>}<br/><br/>function passOutputStatusBuffer(s, raw)<br/>{<br/> if (raw)<br/> {<br/> statusBuffer.buffer += ("[" + getCurrentTimeString() + "] " + s);<br/> refreshCurrentBuffer("status", statusBuffer.buffer);<br/> return;<br/> }<br/><br/> var matched = s.match(/\S+\s+:*(.*)/);<br/> if (matched == null)<br/> {<br/> mapSpecialBuffer["status"].buffer += ("[" + getCurrentTimeString() + "] " + s);<br/> return;<br/> }<br/><br/> statusBuffer.buffer += ("[" + getCurrentTimeString() + "] " + matched[1] + "<br>");<br/> refreshCurrentBuffer("status", statusBuffer.buffer);<br/>}<br/><br/>function passOutputRawBuffer(s)<br/>{<br/> rawBuffer.buffer += ("[" + getCurrentTimeString() + "] " + s + "<br>");<br/> refreshCurrentBuffer("raw", rawBuffer.buffer);<br/>}<br/><br/>function escapeHTML(s)<br/>{<br/> return s.split("<").join("&amp;lt;").split(">").join("&amp;gt;");<br/>}<br/></pre><br/><br/>これらはユーティリティ関数群である。<code>getCurrentTimeString</code>はタイムスタンプ用文字列を生成し、<code>refreshCurrentBuffer</code>はタブが現在表示されているときのみ画面を更新する。<code>passOutputStatusBuffer</code>はstatusタブへ、<code>passOutputRawBuffer</code>はrawタブへそれぞれ文字列を出力する。escapeHTMLはIRCサーバから送られてくる文字列がブラウザによってHTMLとして解釈されないように全てサニタイズするためのフィルタ関数である。<br/><br/><pre class="brush: cpp"><br/>// this function is called from Flash<br/>function output(s)<br/>{<br/> s = escapeHTML(s);<br/><br/> // matched[1] : prefix, matched[2] : command, matched[3] : parameter<br/> var matched = s.match(/^:(\S+)\s(\S+)\s*(.*)/);<br/> if (matched == null)<br/> return;<br/><br/> if (serverName == "")<br/> serverName = matched[1];<br/><br/> passOutputRawBuffer("<font color=red>" + matched[1]<br/> + "</font> <font color=blue>" + matched[2]<br/> + "</font> <font color=green>" + matched[3] + "</font>");<br/><br/> switch (matched[2])<br/> {<br/> case "JOIN":<br/> onJOIN(matched[1], matched[3]);<br/> break;<br/> case "KICK":<br/> onKICK(matched[1], matched[3]);<br/> break;<br/> case "MODE":<br/> onMODE(matched[1], matched[3]);<br/> break;<br/> case "NICK":<br/> onNICK(matched[1], matched[3]);<br/> break;<br/> case "NOTICE":<br/> onNOTICE(matched[1], matched[3]);<br/> break;<br/> case "PART":<br/> onPART(matched[1], matched[3]);<br/> break;<br/> case "PRIVMSG":<br/> onPRIVMSG(matched[1], matched[3]);<br/> break;<br/> case "QUIT":<br/> onQUIT(matched[1], matched[3]);<br/> break;<br/> case "332": // RPL_TOPIC<br/> onTOPIC(matched[3]);<br/> break;<br/> case "353": // RPL_NAMREPLY<br/> onNAMREPLY(matched[3]);<br/> break;<br/> case "001": // RPL_WELCOME<br/> onWELCOME(matched[1], matched[3]);<br/> break;<br/> case "002": // RPL_YOURHOST<br/> case "003": // RPL_CREATED<br/> case "004": // RPL_MYINFO<br/> case "005": // RPL_BOUNCE / RPL_ISUPPORT<br/><br/> case "251": // RPL_LUSERCLIENT<br/> case "254": // RPL_LUSERCHANNELS<br/> case "255": // RPL_LUSERME<br/><br/> case "375": // RPL_MOTDSTART<br/> case "372": // RPL_MOTD<br/> case "376": // RPL_MOTDSTART<br/> passOutputStatusBuffer(matched[3]);<br/> break;<br/> default:<br/><br/> break;<br/> }<br/>}<br/><br/>function reportError(s)<br/>{<br/> statusBuffer.buffer += ("[" + getCurrentTimeString() + "] " + s + "<br>");<br/> refreshCurrentBuffer("status", statusBuffer.buffer);<br/>}<br/></pre><br/><br/><code>output</code>は、先にProxyIRCの解説で登場した、FlashがIRCサーバーからデータを受け取ったときに呼ばれる関数である。ここで正規表現を用いてIRCメッセージを解析し、得られたコマンドに基づいてそれぞれのコマンド処理関数へ処理を分岐させる。<code>reportError</code>はエラーをstatusタブに書き出す関数である。<br/><br/><pre class="brush: cpp"><br/>function renderTabs()<br/>{<br/> var t = "";<br/><br/> for (var i in mapSpecialBuffer)<br/> {<br/> t += mapSpecialBuffer[i].renderTab(i);<br/> }<br/><br/> for (var i in mapChannelBuffer)<br/> {<br/> t += mapChannelBuffer[i].renderTab(i);<br/> }<br/><br/> for (var i in mapPrivateMessageBuffer)<br/> {<br/> t += mapPrivateMessageBuffer[i].renderTab(i);<br/> }<br/><br/> replaceElement("tabs", t);<br/>}<br/><br/>function setCurrentBuffer(name)<br/>{<br/> currentTab = name;<br/> replaceElement("buffer_title", name);<br/>}<br/><br/>function switchTab(name)<br/>{<br/> for (var i in mapSpecialBuffer)<br/> {<br/> if (name == i)<br/> {<br/> setCurrentBuffer(name);<br/> mapSpecialBuffer[name].render();<br/> return;<br/> }<br/> }<br/><br/> for (var i in mapChannelBuffer)<br/> {<br/> if (name == i)<br/> {<br/> setCurrentBuffer(name);<br/> mapChannelBuffer[name].render();<br/> return;<br/> }<br/> }<br/><br/> for (var i in mapPrivateMessageBuffer)<br/> {<br/> if (name == i)<br/> {<br/> setCurrentBuffer(name);<br/> mapPrivateMessageBuffer[name].render();<br/> return;<br/> }<br/> }<br/><br/> alert("Invalid tab");<br/>}<br/><br/>function setCharset(jis)<br/>{<br/> callProxyIRC().setJIS(jis);<br/>}<br/></pre><br/><br/><code>renderTabs</code>は特殊タブ、チャンネルタブ、プライベートメッセージタブの順でタブ列挙バーの描画を行う。setCurrentBufferは現在のタブを設定するとともに表示ウィンドウのタイトルバーに表示されている文字列を現在のタブの名称に更新する。<code>switchTab</code>はユーザがタブを押したときに呼ばれる関数で、目的のタブを探して表示する。<code>setCharset</code>はFlashが日本語JISエンコーディングを文字列に適用するか否かを設定する。<br/><br/><pre class="brush: cpp"><br/>function connect()<br/>{<br/> if (!flashIsEnabled)<br/> {<br/> callProxyIRC().testLoopback();<br/><br/> if (!flashIsEnabled)<br/> {<br/> alert("This client requires Adobe Flash Player 9 and a JavaScript-enabled web browser!");<br/> return;<br/> }<br/> }<br/><br/> document.getElementById("buttonConnect").blur();<br/><br/> if (isConnected)<br/> {<br/> if (!confirm("Do you really want to disconnect?"))<br/> return;<br/><br/> callProxyIRC().quitIRC();<br/> onClose();<br/> return;<br/> }<br/><br/> var n = document.getElementsByName("charset");<br/> for (var i = 0; i < n.length; ++i)<br/> {<br/> if (n.item(i).checked)<br/> callProxyIRC().setJIS(n.item(i).value == "JIS" ? true : false);<br/> }<br/><br/> var h = document.getElementById("IRCHost").value;<br/> if (!h || h == "")<br/> {<br/> alert("Invalid host name");<br/> return;<br/> }<br/><br/> var p = document.getElementById("IRCPort").value;<br/> if (!p || p == "")<br/> {<br/> alert("Invalid port");<br/> return;<br/> }<br/><br/> callProxyIRC().connectIRC(h, parseInt(p, 10));<br/>}<br/><br/>function onConnect()<br/>{<br/> isConnected = true;<br/> document.getElementById("buttonConnect").value = "Disconnect";<br/><br/> switchTab("status");<br/><br/> myNick = document.getElementById("nick").value;<br/><br/> send("USER " + myNick + " " + myNick + " " + myNick + ":" + myNick);<br/> send("NICK " + myNick);<br/>}<br/><br/>function onClose()<br/>{<br/> isConnected = false;<br/> document.getElementById("buttonConnect").value = "Connect";<br/><br/> mapChannelBuffer = new Object();<br/> renderTabs();<br/><br/> switchTab("status");<br/> passOutputStatusBuffer("<br>* Disconnected<br><br>", true);<br/><br/> serverName = "";<br/> myNick = "";<br/>}<br/></pre><br/><br/><code>connect</code>はユーザが接続ボタンを押したときに呼ばれる関数で、接続中に呼ばれると接続を切断するはたらきもある。フォームのホスト欄、ポート欄からそれぞれデータを取得し、Flash側のメソッドを呼び出してIRCサーバへの接続を開始する。<code>onConnect</code>、<code>onClose</code>はそれぞれサーバに接続したとき、切断されたときにFlash側から呼び出される関数で、前者はニックネームを設定するIRCコマンドをサーバへ送信し、後者はチャンネルリストをクリアしてタブ列挙バーを再描画し、画面に切断の表示を出して終了処理を行う。<br/><br/><pre class="brush: cpp"><br/>function joinChannel()<br/>{<br/> document.getElementById("buttonJoin").blur();<br/><br/> if (!isConnected)<br/> {<br/> alert("Not connected");<br/> return;<br/> }<br/><br/> if (document.getElementById("channelName").value == "")<br/> {<br/> alert("No channel specified");<br/> return;<br/> }<br/><br/> if (document.getElementById("channelName").value.charAt(0) != '#')<br/> {<br/> alert("Invalid channel name - it must begin with '#'");<br/> return;<br/> }<br/><br/> send("JOIN " + document.getElementById("channelName").value);<br/>}<br/><br/>function send(s)<br/>{<br/> passOutputRawBuffer("(send) " + s);<br/><br/> callProxyIRC().sendIRC(s);<br/>}<br/><br/>function sendMessage()<br/>{<br/> var m = document.getElementById("message").value;<br/> if (m == "")<br/> return;<br/><br/> if (document.getElementById("raw").checked)<br/> {<br/> document.getElementById("message").value = "";<br/> send(m);<br/> return;<br/> }<br/><br/> var matched = m.match(/\/quote\s+(.*)/i);<br/> if (matched)<br/> {<br/> document.getElementById("message").value = "";<br/> send(matched[1]);<br/> return;<br/> }<br/><br/> for (var i in mapSpecialBuffer)<br/> {<br/> if (currentTab == i)<br/> {<br/> send(m);<br/> return;<br/> }<br/> }<br/><br/> for (var i in mapChannelBuffer)<br/> {<br/> if (currentTab == i)<br/> {<br/> var c = mapChannelBuffer[currentTab];<br/> if (!c)<br/> return;<br/><br/> c.buffer += "[";<br/> c.buffer += getCurrentTimeString();<br/> c.buffer += "] <";<br/> c.buffer += myNick;<br/> c.buffer += "> ";<br/> c.buffer += m;<br/> c.buffer += "<br>";<br/><br/> c.renderBuffer(currentTab);<br/><br/> document.getElementById("message").value = "";<br/> send("PRIVMSG " + currentTab + " :" + m);<br/> return;<br/> }<br/> }<br/><br/> for (var i in mapPrivateMessageBuffer)<br/> {<br/> if (currentTab == i)<br/> {<br/> var pm = mapPrivateMessageBuffer[currentTab];<br/> if (!pm)<br/> return;<br/><br/> matched = currentTab.match(/([^@]+)@/);<br/> if (!matched)<br/> return;<br/><br/> pm.buffer += ("[" + getCurrentTimeString() + "] ");<br/> pm.buffer += "<";<br/> pm.buffer += myNick;<br/> pm.buffer += "> ";<br/> pm.buffer += m;<br/> pm.buffer += "<br>";<br/><br/> pm.renderBuffer(currentTab);<br/><br/> document.getElementById("message").value = "";<br/> send("PRIVMSG " + matched[1] + " :" + m);<br/> return;<br/> }<br/> }<br/>}<br/></pre><br/><br/><code>joinChannel</code>はユーザが操作パネルから参加対象チャンネルを明示的に選んでチャンネルに参加することを選んだ場合に呼ばれる関数である。sendは裸の文字列をサーバへ送信する関数で、<code>sendMessage</code>は ユーザが操作パネルのメッセージ欄に文字列を記入しEnterキーを押すか送信ボタンを押したときに呼ばれる関数である。IRCサーバはユーザが送信して きたメッセージをそのユーザ自身へエコーしないので、クライアントが自分で自分の発したメッセージを画面に書き出してやる必要がある。<br/><br/><pre class="brush: cpp"><br/>function onJOIN(prefix, param)<br/>{<br/> var matched = prefix.match(/([^!]+)!(\S+)/);<br/> if (matched == null)<br/> return;<br/><br/> if (matched[1] == myNick)<br/> {<br/> var c = new ChannelBuffer("");<br/><br/> c.buffer += ("[" + getCurrentTimeString() + "] ");<br/> c.buffer += "<font color=\"green\">* Now talking in ";<br/> c.buffer += param;<br/> c.buffer += "</font><br>";<br/><br/> mapChannelBuffer[param] = c;<br/><br/> renderTabs();<br/> switchTab(param);<br/> send("MODE " + param);<br/> //send("WHO " + param);<br/> }<br/> else<br/> {<br/> var c = mapChannelBuffer[param];<br/> if (!c)<br/> return;<br/><br/> c.buffer += ("[" + getCurrentTimeString() + "] ");<br/> c.buffer += "<font color=\"green\">* Joins: ";<br/> c.buffer += matched[1];<br/> c.buffer += " (";<br/> c.buffer += matched[2];<br/> c.buffer += ")</font><br>";<br/><br/> c.addMember(ChannelMember("", matched[1]));<br/> c.sortMembers();<br/><br/> c.render(param);<br/> }<br/>}<br/><br/>function onPART(prefix, param)<br/>{<br/> var matched = prefix.match(/([^!]+)!(\S+)/);<br/> if (matched == null)<br/> return;<br/><br/> var c = mapChannelBuffer[param];<br/> if (!c)<br/> return;<br/><br/> c.buffer += ("[" + getCurrentTimeString() + "] ");<br/> c.buffer += "<font color=\"green\">* Parts: ";<br/> c.buffer += matched[1];<br/> c.buffer += " (";<br/> c.buffer += matched[2];<br/> c.buffer += ")</font><br>";<br/><br/> c.removeMember(matched[1]);<br/> c.sortMembers();<br/><br/> c.render(param);<br/>}<br/><br/>function onKICK(prefix, param)<br/>{<br/> var matched = prefix.match(/([^!]+)!(\S+)/);<br/> if (matched == null)<br/> return;<br/><br/> var nick = matched[1];<br/><br/> matched = param.match(/(\S+)\s+(\S+)\s+:(.*)/);<br/> if (matched == null || matched[1] == null || matched[2] == null)<br/> return;<br/><br/> if (matched[2] == myNick)<br/> {<br/> delete mapChannelBuffer[matched[1]];<br/><br/> var s = c.buffer += ("[" + getCurrentTimeString() + "] <font color=\"green\">* You were kicked from ");<br/> s += matched[1];<br/> s += " by ";<br/> s += nick;<br/><br/> if (matched[3])<br/> {<br/> s += " (";<br/> s += matched[3];<br/> s += ")";<br/> }<br/><br/> s += "</font><br>";<br/><br/> passOutputStatusBuffer(s, true);<br/><br/> renderTabs();<br/> switchTab("status");<br/><br/> return;<br/> }<br/><br/> var c = mapChannelBuffer[matched[1]];<br/> if (!c)<br/> return;<br/><br/> c.buffer += ("[" + getCurrentTimeString() + "] ");<br/> c.buffer += "<font color=\"green\">* ";<br/> c.buffer += matched[2];<br/> c.buffer += " was kicked by ";<br/> c.buffer += nick;<br/><br/> if (matched[3])<br/> {<br/> c.buffer += " (";<br/> c.buffer += matched[3];<br/> c.buffer += ")";<br/> }<br/><br/> c.buffer += "</font><br>";<br/><br/> c.removeMember(matched[2]);<br/> c.sortMembers();<br/><br/> c.render(param);<br/>}<br/><br/>function onQUIT(prefix, param)<br/>{<br/> var matched = prefix.match(/([^!]+)!/);<br/> if (matched == null)<br/> return;<br/><br/> var nick = matched[1];<br/> for (var i in mapChannelBuffer)<br/> {<br/> var c = mapChannelBuffer[i];<br/> if (c.removeMember(nick))<br/> {<br/> c.buffer += ("[" + getCurrentTimeString() + "] ");<br/> c.buffer += "<font color=\"blue\">*** Quits: ";<br/> c.buffer += matched[1];<br/> c.buffer += " (";<br/> c.buffer += ((param.length != 0 &amp;&amp; param.charAt(0) == ":") ? param.substr(1): param);<br/> c.buffer += ")</font><br>";<br/><br/> c.sortMembers();<br/> c.render(i);<br/> }<br/> }<br/>}<br/></pre><br/><br/>これらはユーザの入退出に関連するメッセージを受け取ったときの処理関数である。<code>onJOIN</code>はユーザがチャンネルに参加してきたときに送られるメッセージで、自分がチャンネルに参加成功したときも送られてくるためそれぞれについて処理する必要がある。<code>onPART</code>はユーザがチャンネルから退出したというメッセージ、<code>onKICK</code>はチャンネルから強制的に出されたというメッセージを処理する。<code>onQUIT</code>はあるユーザがIRCサーバ自体から退出したというメッセージで、<code>onPART</code>と異なりそのユーザが参加していた全てのチャンネルについて影響を考慮してやる必要がある。<br/><br/><pre class="brush: cpp"><br/>function onMODE(prefix, param)<br/>{<br/> var matched = prefix.match(/([^!]+)/);<br/> if (matched == null)<br/> return;<br/><br/> var subject = matched[1];<br/><br/> matched = param.match(/(\S+)\s+(\S+)\s+(\S*)/);<br/> if (matched == null)<br/> return;<br/><br/> var c = mapChannelBuffer[matched[1]];<br/> if (!c)<br/> return;<br/><br/> // channel mode<br/> if (!matched[3])<br/> {<br/> c.buffer += ("[" + getCurrentTimeString() + "] ");<br/> c.buffer += "<font color=\"green\">* ";<br/> c.buffer += subject;<br/> c.buffer += " sets mode: ";<br/> c.buffer += matched[2];<br/> c.buffer += "</font><br>";<br/><br/> c.renderBuffer(matched[1]);<br/> return;<br/> }<br/><br/> // user mode<br/><br/> c.buffer += ("[" + getCurrentTimeString() + "] ");<br/> c.buffer += "<font color=\"green\">* ";<br/> c.buffer += subject;<br/> c.buffer += " sets mode: ";<br/> c.buffer += matched[2];<br/> c.buffer += " ";<br/> c.buffer += matched[3];<br/> c.buffer += "</font><br>";<br/><br/> c.setMemberMode(matched[3], matched[2]);<br/> c.sortMembers();<br/><br/> c.render(matched[1]);<br/>}<br/><br/>function onNICK(prefix, param)<br/>{<br/> var matched = prefix.match(/([^!]+)!/);<br/> if (matched == null)<br/> return;<br/><br/> var subject = matched[1];<br/><br/> matched = param.match(/:(\S+)/);<br/> if (matched == null)<br/> return;<br/><br/> var newNick = matched[1];<br/><br/> var m = ("[" + getCurrentTimeString() + "] <font color=\"green\">* ");<br/> m += subject;<br/> m += " is now known as ";<br/> m += newNick;<br/> m += "</font><br>";<br/><br/> if (subject == myNick)<br/> {<br/> myNick = newNick;<br/> document.getElementById("nick").value = myNick;<br/> }<br/><br/> for (var i in mapChannelBuffer)<br/> {<br/> var c = mapChannelBuffer[i];<br/> var removed = c.removeMember(subject);<br/> if (removed)<br/> {<br/> c.buffer += m;<br/> c.addMember(ChannelMember(removed.prefix, newNick));<br/> c.sortMembers();<br/> c.render(i);<br/> }<br/> }<br/>}<br/></pre><br/><br/><code>onMODE</code>はチャンネルまたはユーザのモードが変化したとき、<code>onNICK</code>はユーザがニックネームを変更したときに送られるメッセージを処理する。ユーザのニックネーム変更は<code>onQUIT</code>同様に全ての関係チャンネルで処理しなければならない。<br/><br/><pre class="brush: cpp"><br/>function onNOTICE(prefix, param)<br/>{<br/> var matched = prefix.match(/([^!]+)/);<br/> if (matched == null)<br/> return;<br/><br/> var nick = matched[1];<br/><br/> matched = param.match(/(\S+)\s+:(.*)/);<br/> if (matched == null)<br/> {<br/> passOutputStatusBuffer("-" + serverName + "- " + param.substr(1) + "<br>", true);<br/> return;<br/> }<br/><br/> var c = mapChannelBuffer[matched[1]];<br/> if (!c)<br/> {<br/> delete mapChannelBuffer[matched[1]];<br/> passOutputStatusBuffer("-" + nick + "- " + matched[2] + "<br>", true);<br/> return;<br/> }<br/><br/> c.buffer += ("[" + getCurrentTimeString() + "] ");<br/> c.buffer += "-> (";<br/> c.buffer += nick;<br/> c.buffer += ") ";<br/> c.buffer += matched[2];<br/> c.buffer += "<br>";<br/><br/> c.renderBuffer(matched[1]);<br/>}<br/><br/>function onPRIVMSG(prefix, param)<br/>{<br/> var matched = prefix.match(/([^!]+)/);<br/> if (matched == null)<br/> return;<br/><br/> var nick = matched[1];<br/><br/> matched = param.match(/(\S+)\s+:(.*)/);<br/> if (matched == null)<br/> return;<br/><br/> var c = mapChannelBuffer[matched[1]];<br/> if (!c)<br/> {<br/> if (matched[1] == myNick)<br/> { // private message<br/> var pm = mapPrivateMessageBuffer[nick + "@" + serverName];<br/> if (pm)<br/> {<br/> pm.buffer += ("[" + getCurrentTimeString() + "] ");<br/> pm.buffer += "<";<br/> pm.buffer += nick;<br/> pm.buffer += "> ";<br/> pm.buffer += matched[2];<br/> pm.buffer += "<br>";<br/><br/> pm.renderBuffer(nick + "@" + serverName);<br/> }<br/> else<br/> {<br/> var pm = new ChannelBuffer("");<br/> pm.buffer += ("[" + getCurrentTimeString() + "] ");<br/> pm.buffer += "<";<br/> pm.buffer += nick;<br/> pm.buffer += "> ";<br/> pm.buffer += matched[2];<br/> pm.buffer += "<br>";<br/><br/> mapPrivateMessageBuffer[nick + "@" + serverName] = pm;<br/><br/> renderTabs();<br/> }<br/> }<br/> return;<br/> }<br/><br/> c.buffer += "[";<br/> c.buffer += getCurrentTimeString();<br/> c.buffer += "] <";<br/> c.buffer += c.getMemberPrefix(nick);<br/> c.buffer += nick;<br/> c.buffer += "> ";<br/> c.buffer += matched[2];<br/> c.buffer += "<br>";<br/><br/> c.renderBuffer(matched[1]);<br/>}<br/><br/>function openPrivateMessage(target)<br/>{<br/> if (serverName == "")<br/> {<br/> alert("Not connected");<br/> return;<br/> }<br/><br/> var matched = target.match(/([^@]+)/);<br/> if (!matched)<br/> return;<br/><br/> var pm = mapPrivateMessageBuffer[matched[1] + "@" + serverName];<br/> if (!pm)<br/> {<br/> var pm = new ChannelBuffer("");<br/> mapPrivateMessageBuffer[matched[1] + "@" + serverName] = pm;<br/><br/> renderTabs();<br/> }<br/><br/> switchTab(matched[1] + "@" + serverName);<br/>}<br/></pre><br/><br/>IRCのチャンネル内で発言するにはPRIVMSGによるかNOTICEによるかのどちらかであり、<code>onNOTICE</code>はそのうちNOTICEメッセージを、<code>onPRIVMSG</code>は PRIVMSGメッセージを扱う。PRIVMSGメッセージは引数がユーザの時には1対1のプライベートメッセージのためのメッセージとしても使用され る。このプライベートメッセージを受け取ると、対象ユーザと会話するためのタブが存在しない場合は新たに1対1会話用のタブを生成して<code>mapPrivateMessageBuffer</code>へ追加する。<code>openPrivateMessage</code>はユーザがチャンネル参加者リストの中の1対1で会話したいメンバーをクリックしたときに新しくタブを生成する関数である。<br/><br/><pre class="brush: cpp"><br/>function onTOPIC(param)<br/>{<br/> var matched = param.match(/\S+\s+(\S+)\s+:(.*)/);<br/> if (matched == null)<br/> return;<br/><br/> var c = mapChannelBuffer[matched[1]];<br/> if (!c)<br/> return;<br/><br/> c.buffer += "[Topic] ";<br/> c.buffer += matched[2];<br/> c.buffer += "<br><br>";<br/><br/> c.render(matched[1]);<br/>}<br/><br/>function onNAMREPLY(param)<br/>{<br/> var matched = param.match(/=\s+(\S+)\s+:(.+)/);<br/> if (matched == null)<br/> return;<br/><br/> var c = mapChannelBuffer[matched[1]];<br/> if (!c)<br/> return;<br/><br/> var a = matched[2].split(" ");<br/> for (var i in a)<br/> {<br/> switch (a[i].charAt(0))<br/> {<br/> case "@":<br/> c.addMember(ChannelMember("@", a[i].substr(1)));<br/> break;<br/> case "+":<br/> c.addMember(ChannelMember("+", a[i].substr(1)));<br/> break;<br/> default:<br/> c.addMember(ChannelMember("", a[i]));<br/> break;<br/> }<br/> }<br/><br/> c.sortMembers();<br/> c.renderMembers(matched[1]);<br/>}<br/><br/>function onWELCOME(prefix, param)<br/>{<br/> // it's not necessarily true that welcome message is the first one the server sents in<br/> if (serverName != prefix)<br/> serverName = prefix;<br/><br/> var matched = param.match(/(\S+)\s+:*/);<br/> if (matched != null)<br/> {<br/> myNick = matched[1];<br/> document.getElementById("nick").value = myNick;<br/> }<br/><br/> passOutputStatusBuffer(param);<br/>}<br/></pre><br/><br/><code>onTOPIC</code>はチャンネルトピックを通知するメッセージを、<code>onNAMEREPLY</code>はチャンネル参加者を列挙するメッセージを処理する。双方ともチャンネル参加時に送られてくるメッセージである。それに対し、<code>onWELCOME</code>は申請したニックネームがサーバに登録されサーバに受容されたときに送られてくるもので、ユーザのニックネームを含んでおり、申請したニックネームを重複などの理由によりサーバが勝手に変更した場合にも対処できるようにしてある。<br/><br/><pre class="brush: cpp"><br/>function onXButtonClick()<br/>{<br/> for (var i in mapSpecialBuffer)<br/> {<br/> if (i == currentTab)<br/> {<br/> alert("This tab can't be closed");<br/> return;<br/> }<br/> }<br/><br/> for (var i in mapChannelBuffer)<br/> {<br/> if (i == currentTab)<br/> {<br/> send("PART " + currentTab);<br/><br/> delete mapChannelBuffer[currentTab];<br/><br/> renderTabs();<br/> switchTab("status");<br/> return;<br/> }<br/> }<br/><br/> for (var i in mapPrivateMessageBuffer)<br/> {<br/> if (i == currentTab)<br/> {<br/> delete mapPrivateMessageBuffer[currentTab];<br/><br/> renderTabs();<br/> switchTab("status");<br/> return;<br/> }<br/> }<br/><br/> alert("Invalid buffer");<br/>}<br/><br/>function onClearThisBuffer()<br/>{<br/> document.getElementById("buttonClearBuffer").blur();<br/><br/> if (!confirm("Do you really want to clear the content of the current buffer?"))<br/> return;<br/><br/> for (var i in mapSpecialBuffer)<br/> {<br/> if (currentTab == i)<br/> {<br/> mapSpecialBuffer[currentTab].buffer = "";<br/> mapSpecialBuffer[currentTab].renderBuffer(currentTab);<br/> return;<br/> }<br/> }<br/><br/> for (var i in mapChannelBuffer)<br/> {<br/> if (currentTab == i)<br/> {<br/> mapChannelBuffer[currentTab].buffer = "";<br/> mapChannelBuffer[currentTab].renderBuffer(currentTab);<br/> return;<br/> }<br/> }<br/><br/> for (var i in mapPrivateMessageBuffer)<br/> {<br/> if (currentTab == i)<br/> {<br/> mapPrivateMessageBuffer[currentTab].buffer = "";<br/> mapPrivateMessageBuffer[currentTab].renderBuffer(currentTab);<br/> return;<br/> }<br/> }<br/><br/> alert("Invalid buffer");<br/>}<br/></pre><br/><br/><code>onXButtonClick</code>は、上述のように、タブ右上のXボタンを押すとそのタブを閉じることが出来るというもので、同時にそのタブが表象していたチャンネルを離脱するようになっている。<code>onClearThisBuffer</code>は、操作パネルのタブの内容をクリアするボタンを押したときの動作を記述している。<br/><br/><pre class="brush: cpp"><br/>function loadChannelList()<br/>{<br/> if (!isConnected)<br/> {<br/> alert("Not connected");<br/> return;<br/> }<br/><br/> channelListBuffer.buffer += "Now loading...";<br/> refreshCurrentBuffer("channels", channelListBuffer.buffer);<br/><br/> sendHTTP('' , 'GET', './channels', showChannels, true);<br/>}<br/><br/>function showChannels(hr)<br/>{<br/> var nodelist = hr.responseXML.getElementsByTagName("Channel");<br/> if (!nodelist)<br/> {<br/> channelListBuffer.buffer = channelListHeader;<br/> channelListBuffer.buffer += "Currently there are no channels on the server.";<br/> refreshCurrentBuffer("channels", channelListBuffer.buffer);<br/><br/> return;<br/> }<br/><br/> var output = nodelist.length.toString();<br/> output += (nodelist.length == 1 ? " channel has been retrieved.<br><br>" : " channels have been retrieved.<br><br>");<br/><br/> for (var i = 0; i < nodelist.length; ++i)<br/> {<br/> var n = nodelist.item(i);<br/> if (n == null)<br/> continue;<br/><br/> var users = n.getAttribute("Users");<br/><br/> var nn = n.firstChild;<br/> var name = "";<br/> var topic = "";<br/><br/> while (nn != null)<br/> {<br/> if (nn.nodeName == "Name")<br/> {<br/> // nn.textContent == Mozilla only<br/> var a = nn.firstChild;<br/> // NODE_TEXT == 3 || NODE_CDATA_SECTION == 4<br/> while (a != null &amp;&amp; (a.nodeType == 3 || a.nodeType == 4))<br/> {<br/> name = a.nodeValue;<br/> a = a.nextSibling;<br/> }<br/> }<br/> else if (nn.nodeName == "Topic")<br/> {<br/> var a = nn.firstChild;<br/> // NODE_TEXT == 3 || NODE_CDATA_SECTION == 4<br/> while (a != null &amp;&amp; (a.nodeType == 3 || a.nodeType == 4))<br/> {<br/> topic = a.nodeValue;<br/> a = a.nextSibling;<br/> }<br/> }<br/><br/> nn = nn.nextSibling;<br/> }<br/><br/> output += (i + 1).toString();<br/> output += ". ";<br/> output += "<a style=\"text-decoration: none;\" href=\"#\" onclick=\"joinChannelFromList('#";<br/> output += name;<br/> output += "')\">#";<br/> output += name;<br/> output += "</a> ";<br/> output += topic;<br/> output += " (";<br/> output += users;<br/> output += " users)<br><br>";<br/> }<br/><br/> channelListBuffer.buffer = channelListHeader;<br/> channelListBuffer.buffer += output;<br/><br/> refreshCurrentBuffer("channels", channelListBuffer.buffer);<br/>}<br/><br/>function joinChannelFromList(channelName)<br/>{<br/> for (var i in mapChannelBuffer)<br/> {<br/> if (i == channelName)<br/> {<br/> switchTab(channelName);<br/> return;<br/> }<br/> }<br/><br/> send("JOIN " + channelName);<br/>}<br/></pre><br/><br/><code>loadChannelList</code>は、AJAXでサーバからIRCチャンネルリストを得る関数である。<a href="../../dice/index_j.html" target="_blank">DICE</a>は、/channelsパスにアクセスするとIRCチャンネルリストをXMLで返す。IRC側にもチャンネルリストを得るコマンドは勿論存在するが、ここでは元々Webチャンネルブラウザのために実装したインターフェイスを再利用している。そのコールバック関数が<code>showChannels</code>で、AJAXリクエスト完了時に受け取ったXMLを処理する。<code>joinChannelFromList</code>は、チャンネルリスト上のチャンネル名をクリックしたときに呼ばれる関数で、目的のチャンネルへユーザを参加させる。<br/><br/><pre class="brush: cpp"><br/>function onKeyDown(e)<br/>{<br/> if (e.keyCode == 13 &amp;&amp; isConnected) // Enter<br/> {<br/> sendMessage();<br/> }<br/>}<br/> </pre><br/><br/>これがJavaScriptコードの最後の部分で、先に挙げたHTMLのデザイン部分が後に続く。<code>onKeyDown</code>はキー入力を監視し、Enterキーが押されるとメッセージ欄に記入されているメッセージを送信する。このバージョンではIEだとEnterキー押下時に音が鳴るが、formタグの使用を止めれば回避できる。このIRCクライアントWebアプリケーションは<a href="../../dice/index_j.html" target="_blank">DICE</a>のパッケージに同梱されているので、デザインも一新されている最新版での実際の動作は<a href="../../dice/index_j.html" target="_blank">DICE</a>をインストールして確かめてみて欲しい(本記事で掲載しているバージョンのActionScriptコードに存在する日本語JISコードの取り扱いに関するバグはそちらでは修正済みである)。<br/><br/>AJAXやWeb 2.0と形容されるものを超えた先にあるのは何か? WebブラウザがHTTPではないプロトコルのサーバに自由に接続することが当たり前のようになるとき、WebブラウザはWebを超えたプラットフォームとなり、Webそのものの刷新が起こるのではないか。そのときWebブラウザはWebブラウザではない他の存在に、WebはWebではない他の何かになっているのかもしれない。<br/><br/>(2008-04-11追記) 現時点で、Adobe Flash Player 9.0.124.0でのFlashのセキュリティ仕様変更により、本稿のクライアントを動作させるにはサーバ側の対応が必要となる。<a href="../../dice/index_j.html" target="_blank">DICE</a>ではバージョン0.9.1.1で対応完了している。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-15910416964542205562006-11-09T15:44:00.000+09:002015-02-26T02:45:20.583+09:00C++ and C#/.NET Interoperability for RSA Public-key Cryptography andAES Symmetric CipherWhen you have to write a secure network application, cryptography is one of the topics you can't escape from. In most cases there are high-level packages such as SSL available, but it's not always like that and you may have to go lower-level. Besides, even if you don't program a custom security solution by yourself, it's not a bad idea to know how these secure protocols actually work as it helps you to choose a right solution for your problem. This article provides a basic idea of secure communication by illustrating C++ and C# code examples. Also this article will be useful for those who writes a custom secure protocol between a C++ application and a C# application. (Disclaimer: but don't use the example explained here as is in your mission-critical application! This article is only for the education purpose. Realworld secure communication libraries implement countermeasures against many kinds of known cryptographic attacks while this sample unfortunately doesn't.)<br/><a name='more'></a><br/>Regarding the sample code of this article, the C# side is based on .NET Framework 1.1 and the C++ side is based on <a href="http://www.cryptopp.com/" target="_blank">Crypto++ Library </a> 5.2.1. .NET Framework is an obvious solution for a .NET application and it has necessary tools for security albeit lacks a bit of variety. On the other hand <a href="http://www.cryptopp.com/" target="_blank">Crypto++ Library</a> is a full of variety and even supports many obscure crypto algorithms in the Open-Source form. Though the Platform SDK provided by Microsoft also offers a security framework and related utilities, I prefer <a href="http://www.cryptopp.com/" target="_blank">Crypto++ Library</a> because it's cross-platform and Open Source with a lenient license. It is updated rather quickly considering it's maintained by a single person. The only point in <a href="http://www.cryptopp.com/" target="_blank">Crypto++ Library</a> that may not be easy for a newcomer is its relatively complicated framework based on C++ template. In the C++ example of this article I'll show you minimal but effective use of it.<br/><br/>The basic flow and interaction between these C# and C++ programs is like this:<br/><br/>1. The C# program generates a 1024-bit RSA public and private key pair (<code>pubkey.txt</code> and <code>prvkey.txt</code> respectively)<br/>2. The C++ program receives the public key (<code>pubkey.txt</code>). It generates a 128-bit key and a 128-bit initialization vector (IV) for 128-bit AES symmetric cipher and encrypts them using the public key sent from the C# program, producing a ciphertext (<code>cipher.txt</code>). The key and the IV in a plaintext are saved locally (<code>aeskey.txt</code>) while the ciphertext is sent to the C# program.<br/>3. The C# program decrypts the ciphertext (<code>cipher.txt</code>) with the RSA private key. With the obtained key and IV, it encrypts a message and sends the resulted ciphertext (<code>aescipher.txt</code>) to the C++ program.<br/>4. The C++ program receives the ciphertext and decrypts it with the AES key (<code>aeskey.txt</code>).<br/><br/>The step 1 and 2 are the preparation for establishing a secure channel. The 3 and 4 are the parts that does actual message exchange. As you notice, establishing a secure channel equals secure key exchange. It exchanges an AES key under the protection of RSA public-key crypto.<br/><br/>To enable secure key exchange, a public-key cryptographic algorithm is essential. Anyone can use a public key to encrypt a message, but only the one with its correspondent private key can decrypt it. As the US patent for the RSA algorithm expired some years ago, it's the most popular public-key crypto algorithm. No wonder the .NET framework supports it. However, you have to understand the key format to exchange a public key between a .NET application and a C++ application as there's no agreement on which format to use. In the example code I'll show you the most straightforward way to exchange an RSA public key, which in fact doesn't encode it in a certain format and passes it in its raw form. Subsequent messages after key exchange are encrypted by the AES symmetric cipher because public-key encryption eats relatively large CPU resources. Symmetric cipher (dubbed "block cipher" due to the encryption method) has various operation modes and the one that uses an IV is more secure than the most basic mode.<br/><br/>Let's take a look at the C# code for a console application <code>RSATestCSharp.exe</code>. When it's executed without an argument, it shows the usage help. Executing it with the "gen" argument does the step 1 in the explanation above. Giving the "dec" argument results in the step 3. You have to manually copy ciphertext files between the C# program and the C++ program.<br/><br/><pre class="brush: cpp"><br/>using System;<br/>using System.Security.Cryptography;<br/>using System.IO;<br/>using System.Text;<br/><br/>namespace RSATestCSharp<br/>{<br/> /// <summary><br/> /// Summary description for Class1.<br/> /// </summary><br/> class Class1<br/> {<br/> /// <summary><br/> /// The main entry point for the application.<br/> /// </summary><br/> [STAThread]<br/> static void Main(string[] args)<br/> {<br/> //<br/> // TODO: Add code to start application here<br/> //<br/><br/> if (args.Length == 0)<br/> {<br/> Console.WriteLine(@"Usage:<br/>To generate an RSA private/public key pair: type ""RSATestCSharp gen""<br/>To decrypt an RSA ciphertext: type ""RSATestCSharp dec""<br/>");<br/> return;<br/> }<br/><br/> try<br/> {<br/> RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(1024);<br/><br/> if (args[0] == "gen")<br/> {<br/> RSAParameters rp = RSA.ExportParameters(false);<br/> Console.WriteLine("PubKey Modulus size: {0:D}", rp.Modulus.Length);<br/> Console.WriteLine("PubKey Exponent size: {0:D}", rp.Exponent.Length);<br/><br/> // exports a modulus and a public exponent<br/> using (FileStream fs = new FileStream("pubkey.txt", FileMode.Create))<br/> {<br/> BinaryWriter sw = new BinaryWriter(fs);<br/> sw.Write(rp.Modulus);<br/> sw.Write(rp.Exponent);<br/> }<br/><br/> // exports a modulus and a private exponent, in XML<br/> using (StreamWriter sw = new StreamWriter("prvkey.txt"))<br/> {<br/> sw.Write(RSA.ToXmlString(true));<br/> }<br/><br/> Console.WriteLine("Key generation is done.");<br/> return;<br/> }<br/><br/> using (StreamReader sr = new StreamReader("prvkey.txt"))<br/> {<br/> string line;<br/> string x = "";<br/> while ((line = sr.ReadLine()) != null)<br/> {<br/> x += line;<br/> }<br/> try<br/> {<br/> RSA.FromXmlString(x);<br/> }<br/> catch (CryptographicException e)<br/> {<br/> Console.WriteLine(e.ToString());<br/> }<br/> }<br/><br/> byte[] enc;<br/> using (FileStream fs = new FileStream("cipher.txt", FileMode.Open, FileAccess.Read))<br/> {<br/> byte[] buffer = new byte[8192];<br/> using (MemoryStream ms = new MemoryStream())<br/> {<br/> while (true)<br/> {<br/> int read = fs.Read(buffer, 0, buffer.Length);<br/> if (read <= 0)<br/> {<br/> enc = ms.ToArray();<br/> break;<br/> }<br/> ms.Write (buffer, 0, read);<br/> }<br/> }<br/> }<br/><br/> Console.WriteLine("RSA ciphertext size: {0:D} bytes", enc.Length);<br/><br/> RSA.ImportParameters(RSA.ExportParameters(true));<br/> byte[] dec = RSA.Decrypt(enc, true);<br/><br/> if (dec.Length != 32)<br/> {<br/> Console.WriteLine("Invalid plaintext size");<br/> return;<br/> }<br/><br/> // this plaintext is the key + the IV<br/> Console.WriteLine("Plaintext size: {0:D} bytes", dec.Length);<br/><br/> RijndaelManaged myRijndael = new RijndaelManaged();<br/><br/> myRijndael.KeySize = 128;<br/><br/> Console.WriteLine("Rijndael KeySize: {0:D} bits", myRijndael.KeySize);<br/> Console.WriteLine("Rijndael BlockSize: {0:D} bits", myRijndael.BlockSize);<br/> Console.WriteLine("Rijndael Padding: {0}", myRijndael.Padding.ToString());<br/> Console.WriteLine("Rijndael Mode: {0}", myRijndael.Mode.ToString());<br/><br/> byte[] key = new byte[16];<br/> byte[] IV = new byte[16];<br/> Array.Copy(dec, 0, key, 0, 16);<br/> Array.Copy(dec, 16, IV, 0, 16);<br/><br/> ICryptoTransform encryptor = myRijndael.CreateEncryptor(key, IV);<br/><br/> MemoryStream msEnc = new MemoryStream();<br/> CryptoStream cs = new CryptoStream(msEnc, encryptor, CryptoStreamMode.Write);<br/><br/> byte[] plaintext = {(byte)'t', (byte)'e', (byte)'s', (byte)'t'};<br/> Console.WriteLine("Rijndael original size: {0:D} bytes", plaintext.Length);<br/><br/> cs.Write(plaintext, 0, plaintext.Length);<br/> cs.FlushFinalBlock();<br/><br/> byte[] ciphertext = msEnc.ToArray();<br/> Console.WriteLine("Rijndael encrypted Size: {0:D} bytes", ciphertext.Length);<br/><br/> using (FileStream fs = new FileStream("aescipher.txt", FileMode.Create))<br/> {<br/> BinaryWriter sw = new BinaryWriter(fs);<br/> sw.Write(ciphertext);<br/> }<br/><br/> Console.WriteLine("Done");<br/> }<br/> catch(Exception e)<br/> {<br/> Console.WriteLine("{0}: {1}", e.StackTrace, e.Message);<br/> }<br/> }<br/> }<br/>}<br/></pre><br/><br/>An RSA public key is composed of 2 parts, a modulus and a public exponent. A modulus is a product of 2 random prime numbers. For 1024-bit RSA, its modulus becomes 1024-bit (128 bytes). A public exponent is an arbitrary integer. To pass a public key to the C++ program, this program exports a modulus and a public key in a binary format. A private key can be easily exported to the XML format by using the <code>ToXmlString</code> method of the <code>RSACryptoServiceProvider</code> class. When decrypting a message encrypted by a public key it deserializes a private key from an XML file by <code>FromXmlString</code>.<br/><br/>The real name of the algorithm used in the AES symmetric cipher is called Rijndael. In .NET Framework 1.1 only a Managed implementation of the algorithm is provided and the performance may suffer compared to a native implementation, but it won't matter for most use cases. When you execute it with the "dec" argument after it received a cipher text, you'll see the default padding method for the Rijndael implementation in .NET is PKCS7 as shown by <code>myRijndael.Padding.ToString()</code>, and the symmetric cipher mode is CBC (cipher-block chaining) as shown by <code>myRijndael.Mode.ToString()</code>. The CBC mode uses an IV. The C++ program has to use PKCS#7 and CBC to communicate with the C# program. RSA also does padding for a message block and in this case the method called OAEP (Optimal Asymmetric Encryption Padding) is used. The second parameter of <code>RSA.Decrypt</code> is set true to make it aware of OAEP. As the C++ program chooses a 128-bit block for AES, it assumes the size of key and IV are both 16 bytes. The plaintext message is 4-bytes length and has no terminating null character.<br/><br/>The code for the C++ side (RSATestCPP) is as follows. When it's called with the "rsa" it executes the step 2 in the flow, and with "aes" the step 4. You have to build <a href="http://www.cryptopp.com/" target="_blank">Crypto++ Library</a> and put its include path and library path in your project to build this code.<br/><br/><pre class="brush: cpp"><br/>#include <string><br/><br/>#include <iostream><br/>#include <fstream><br/><br/>// CryptoPP<br/>#include "sha.h"<br/>#include "rsa.h"<br/>#include "hex.h"<br/>#include "osrng.h"<br/>#include "secblock.h"<br/>#include "aes.h"<br/>#include "modes.h"<br/><br/>using namespace std;<br/><br/>int _tmain(int argc, _TCHAR* argv[])<br/>{<br/> if (argc == 1)<br/> {<br/> cout << "Usage:" << endl<br/> << "To encrypt an RSA ciphertext: type \"RSATestCPP rsa\"" << endl<br/> << "To decrypt an AES ciphertext: type \"RSATestCPP aes\"" << endl;<br/> return 0;<br/> }<br/><br/> if (argc != 2)<br/> {<br/> cout << "Unknown option" << endl;<br/> return 0;<br/> }<br/><br/> if (string(argv[1]) == "rsa")<br/> {<br/> string strPublicKey;<br/> ifstream fi("pubkey.txt", ios::in | ios::binary);<br/> if (!fi)<br/> {<br/> cout << "Can't open pubkey.txt" << endl;<br/> return 0;<br/> }<br/><br/> char buf[8192];<br/> while (fi)<br/> {<br/> fi.read(buf, 8192);<br/> strPublicKey.append(buf, fi.gcount());<br/> }<br/><br/> if (strPublicKey.size() <= 128)<br/> {<br/> cout << "Invalid public key size" << endl;<br/> return 1;<br/> }<br/><br/> CryptoPP::RSAES_OAEP_SHA_Encryptor pub;<br/><br/> CryptoPP::Integer nModulus((const byte*)(strPublicKey.data()), 128);<br/> CryptoPP::RSAFunction&amp; r = pub.AccessKey();<br/> r.SetModulus(nModulus);<br/><br/> CryptoPP::Integer nExponent((const byte*)(strPublicKey.data()) + 128, strPublicKey.size() - 128);<br/> r.SetPublicExponent(nExponent);<br/><br/> byte key[16];<br/> byte IV[16];<br/><br/> CryptoPP::OS_GenerateRandomBlock(false, key, 16);<br/> CryptoPP::OS_GenerateRandomBlock(false, IV, 16);<br/><br/> string strPlainText((char*)key, 16);<br/> strPlainText.append((char*)IV, 16);<br/><br/> CryptoPP::AutoSeededRandomPool randPool;<br/> string strCipherText;<br/> CryptoPP::StringSource(<br/> strPlainText,<br/> true,<br/> new CryptoPP::PK_EncryptorFilter(<br/> randPool,<br/> pub,<br/> new CryptoPP::StringSink(strCipherText)<br/> )<br/> );<br/><br/> ofstream out("cipher.txt", ios::out | ios::binary);<br/> if (!out)<br/> {<br/> cout << "Can't open cipher.txt" << endl;<br/> return 0;<br/> }<br/> out.write(strCipherText.data(), strCipherText.size());<br/><br/> ofstream out2("aeskey.txt", ios::out | ios::binary);<br/> if (!out2)<br/> {<br/> cout << "Can't open aeskey.txt" << endl;<br/> return 0;<br/> }<br/> out2.write(strPlainText.data(), strPlainText.size());<br/><br/> cout << "Done" << endl;<br/><br/> return 0;<br/> }<br/><br/> string strAES;<br/> ifstream fi("aescipher.txt", ios::in | ios::binary);<br/> if (!fi)<br/> {<br/> cout << "Can't open aescipher.txt" << endl;<br/> return 0;<br/> }<br/><br/> char buf[8192];<br/> while (fi)<br/> {<br/> fi.read(buf, 8192);<br/> strAES.append(buf, fi.gcount());<br/> }<br/><br/> string strTmp;<br/> ifstream fi2("aeskey.txt", ios::in | ios::binary);<br/> if (!fi2)<br/> {<br/> cout << "Can't open aeskey.txt" << endl;<br/> return 0;<br/> }<br/> while (fi2)<br/> {<br/> fi2.read(buf, 8192);<br/> strTmp.append(buf, fi2.gcount());<br/> }<br/><br/> if (strTmp.size() != 16 + 16)<br/> {<br/> cout << "Invalid key/IV size" << endl;<br/> return 1;<br/> }<br/><br/> byte pDst[16];<br/><br/> CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption d;<br/> d.SetKeyWithIV((const byte*)strTmp.data(), 16, (const byte*)(strTmp.data() + 16));<br/> d.ProcessData((byte*)pDst, (const byte*)(strAES.data()), strAES.size());<br/><br/> // Stripping PKCS#7 padding bytes<br/> unsigned int dwDstSize = strAES.size() - (unsigned int)(pDst[strAES.size() - 1]);<br/><br/> if (dwDstSize >= 16)<br/> {<br/> cout << "Invalid plaintext size: " << dwDstSize << " bytes" << endl;<br/> return 1;<br/> }<br/><br/> string strPlainText((char*)pDst, dwDstSize);<br/><br/> cout << "plaintext: " << strPlainText << " (" << dwDstSize << " bytes)" << endl;<br/><br/> return 0;<br/>}<br/> </pre><br/><br/>Conveniently <a href="http://www.cryptopp.com/" target="_blank">Crypto++ Library</a> predefines <code>CryptoPP::RSAES_OAEP_SHA_Encryptor</code> as one of the RSA encryption schemes in PKCS #1 v2.0. It can interact with the C# counterpart with OAEP without a hassle. <code>CryptoPP::Integer</code> is a CryptoPP class that can hold an integer with arbitrary figures. Cryptography often uses these big integers such as a public key modulus for RSA which is 1024-bit (128 bytes) in this sample code. It imports a modulus and an exponent from the binary file and sets them in the properties of the RSA encryptor object that is an instance of the <code>CryptoPP::RSAES_OAEP_SHA_Encryptor</code> class. The usage of <code>StringSource</code> and <code>StringSink</code> in Crypto++ may be a bit complicated for those who are not familiar with the crypto library, but the basic idea is you set source data in <code>StringSource</code> with a transformation filter that has a destination data in its own parameter. A destination data for a filter must be wrapped with <code>StringSink</code>. These <code>StringSource</code> / <code>StringSink</code> classes can handle the standard C++ classes such as <code>basic_string</code> fairly well.<br/><br/>For an AES key and an IV, it sets 128-bit (16 bytes) random data in them. Each message block for 128-bit AES is 128-bit too. The last block in a message that is smaller than 16 bytes needs padding. After setting a key and IV by <code>SetKeyWithIV</code> the AES decryptor decrypts the ciphertext with <code>ProcessData</code>. As you know the plaintext is 4-bytes length, so this block has paddings. In PKCS#7, if actual padding is 3 bytes the last 3 bytes are filled with 0x03. This sample code manually strips PKCS#7 padding. After the padding is removed, the plaintext will be recovered.久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-67114107940351260992005-05-06T16:55:00.000+09:002015-02-26T02:44:23.992+09:00Thank You Falettinme Be Mice Elf AginDICEの更新日付を見たらなんと1年ぶりの更新だった。このウェブサイトについても同様である。実はDICE 0.85自体は去年の8月に完成していたのだけれど、それから色々あって手を付けることが出来なくなり、リリースそのものは今日に至ったという有様だ。書きたいことも多々あるが、しかしまたしても時間がない。後日に譲る。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-15086769885926219232005-05-05T15:29:00.000+09:002015-02-26T02:47:11.452+09:00How to Programmatically Create Self-signed Certificate and Key PairAssociation for SSL Communication with Microsoft Windows SSPIIn late 2001 when I <a href="http://zzz.zggg.com/2002/04/04/early-dice-design-notes/">started</a> the development of the <a href="http://zzz.zggg.com/dice/" target="_blank">DICE</a>, a multi-protocol network server, one of the planned features was secure authenticated connection across the web for remote server administration. I implemented it with <a href="http://msdn2.microsoft.com/en-us/library/aa380493.aspx" target="_blank">SSPI</a> (Security Support Provider Interface Architecture) found in Microsoft Platform SDK. SSPI is an abstraction framework through which you can control 3 (or more) different secure authentication/communication protocols including SSL (Secure Sockets Layer).<br/><br/>Among the protocols supported in SSPI, I chose SSL because others (NTLM and Kerberos) were useless in my context over the internet without ActiveDirectory and related mess. But SSL in SSPI has some caveats before use - Since SSL is an inefficient streamed protocol unlike others and the abstraction by SSPI is not in high-level, your code starts to look nasty if you attempt to make it conform to the streaming nature of the protocol. It gets worse especially when your application is constructed around asynchronous sockets. Besides, you need a server certificate prior to negotiation.<br/><a name='more'></a><br/>Though I chose SSPI, an Open Source solution for SSL is also publically available. <a href="http://www.openssl.org/" target="_blank">OpenSSL</a>, the Open Source toolkit for SSL/TLS, is what people instantly bring up when there's need of SSL in the OSS world. However it's tightly united with the BSD socket (the situation may be changed as of now though) and not straightforwardly usable in my application that is fundamentally based on asyncronous socket connections. Eventually I could use OpenSSL in an unexpected place - certificate generation. My original intention was to let my application generate a server certificate programmatically, but I couldn't because of the restriction in my time and programming skill back then. Using some of OpenSSL APIs in my application to generate a certificate also slipped my mind and I didn't so much as bother to see if it's possible or not. I passed that task over to a user by bundling a simple certificate generator in my application package: a Windows executable of OpenSSL and a batch script to run it. This solution was far from smart and had irritated me for a long time. After I improved other important parts of my application in later versions, I removed all SSPI-related code and replaced SSL with my own simple protocol that rings well with the other parts of the code in terms of space efficiency.<br/><br/>Later in the early summer of 2004 I had some time to consider new features for the forthcomig version of the <a href="../../dice/" target="_blank">DICE</a>. One of them was IRC over SSL. Though IRC over SSL is not in the IRC standard and I was not too inclined to implement it with not so delightful experience with SSL as I wrote above, I'd already got a few emails that asked it for the <a href="../../dice/" target="_blank">DICE</a>. I browsed SSL-related resources in the MSDN Library for a while and found it looked more accessible than the first time I read it, perhaps because of my ever growing programming skill (ahem). In the following week I implemented IRC over SSL. It was not a trivial task, I had to spend quite some time to investigate why my server couldn't communicate with an IRC client that attempts SSL connection. But finally I could iron out all the problems in SSL handshake and subsequent exchange of encrypted messages - except for certificate generation and how to install a generated certificate in the Windows certificate store. Therefore this document was prepared to tell you how I could solve this little but troublesome problem.<br/><br/>When I made a simple certificate generator with OpenSSL before, it could generate a PFX file that contains a key pair (a public key and a private key) and a certificate associated with the pair. A PFX file is packed in the standard file format called PKCS#12. When you click a file with .pfx extension in the Windows shell (Explorer), it prompts you to install the certificate and the associated key pair in the certificate store of Windows. When your application wants to create an SSL session using SSPI, it has to access the certificate store to retrieve a certificate and a key pair. How can you automate this process by a code without user interaction? If you have enough time to read and understand documents about the PKCS#12 format and the certificate generation algorithm, of course no problem there. If you can embed OpenSSL in your application, it's all right too (you can even learn how OpenSSL creates a PFX file by reading its source code). However, only with Microsoft Platform SDK, how can you tackle this problem?<br/><br/>In my previous attempt, the particular point that prevented me from implementing it in the <a href="../../dice/" target="_blank">DICE</a> was, association of a key pair with a certificate in the certificate store. Without this association, an API call to get a credential for SSL authentication fails. Certificate generation doesn't require much effort because in a minute you'll find the useful function verbosely named <code>CertCreateSelfSignCertificate</code> if a self-signed certificate is enough for your use. But there is no straightforward way to associate a key pair with a certificate in the certificate store at least in the MSDN Library. I wrote a test program to create a certificate and install it in the machine certificate store (since my application is a Windows Service it has to use the machine store, not the user store). But everytime I exported an installed certificate from the store, it refused to export a private key with it and I knew I failed.<br/><br/>What I tried next is to browse code samples. The Platform SDK package contains the <code>Samples</code> folder that contains code snippets correspondent to various categories covered by the SDK. What you 'd be interested in for certificate manipulation is found in the <code>Samples\Security\CryptoApi\CreateCert</code> folder where you can locate the <code>CreateCert</code> program source code. What it does is to create a self-signed certificate for testing use. Optionally you can set various properties of a certificate and put it into the certificate stores. I compiled & linked the <code>CreateCert</code> and could run it to do my work... NOT. It lacked one particular feature - it couldn't put a private key of a key pair in a certificate store. When you try to import a generated self-signed certificate and a key pair into a certificate store, a private key is always lost in the process and you can't export it with the certificate later. It means you can't use the certificate imported by the program as a certificate for SSL communication. In other words without your own private key you can't decrypt a ciphertext encrypted with your public key in an SSL channel. At the end of the program it looks like this<br/><br/><pre class="brush: cpp"><br/> // Set Certificate's Key Provider info<br/> bResult = CertSetCertificateContextProperty(<br/> pCertContext,<br/> CERT_KEY_PROV_INFO_PROP_ID,<br/> 0,<br/> (LPVOID)&CryptKeyProvInfo<br/> );<br/></pre><br/><br/>but it's useless in this problem about a private key. This call to <code>CertSetCertificateContextProperty</code> comes after it places a certificate in a certificate store. Apparently what it has to do is associate a private key with the certificate in the store where it puts the key a moment before, so this is the place where you should do <em>something</em>. However, the <code>CreateCert</code> program doesn't have any clue on it.<br/><br/>Fortunately what Microsoft offer you doesn't end there. You have another tool to manipulate a certificate, but not in the <code>Samples</code> folder. Instead, it's in the <code>Bin</code> folder, and called <code>makecert.exe</code>. Yes, it's a binary executable file and naturally comes without the source code. Nevertheless you can assume it has to be able to do basic tasks in certificate manipulation, such as association of a private key with a certificate stored in a certificate store, since without being able to create a working certificate in a certificate store the purpose of this tool is almost void (OK I'm exaggerating but it's still logical to make a conjecture like this). If you run <code>makecert.exe</code> to generate a self-signed certificate and place it in a certificate with a key pair, it can show you the expected result indeed, unlike the <code>CreateCert</code> program. The imported certificate can be exported complete with its private key if you enter the password. So you can guess something is wrong in the <code>CreateCert</code> source code. The <code>CreateCert</code> program seems to be able to do the same thing as <code>makecert.exe </code>does, but something is definitely missing from it.<br/><br/>This is a typical situation where MSDN and Google are supposed to be your friends. But they couldn't help when I asked my question. I'd assumed the how-to on this problem would be in some FAQ about SSL usage on Windows, but I was wrong. It's as if very few people actually used the certificate-related API in the Microsoft platform. The only place it was mentioned I managed to find was in some messages in the <code>microsoft.public.security.crypto</code> newsgroup. It's the newsgroup where the people responsible for Microsoft security products (sometimes) answer questions about the security-related parts of the Microsoft SDK. I found an MS employee or an MSVP wrote "...then you have to associate the private key with the certificate you imported, but it's a bit tricky." and the message had no hints on how it's tricky nor how you can do it. Yeah, it's tricky I KNOW...<br/><br/>Actually that's all what I could collect from the web and MSDN. If <code>makecert.exe</code> were an Open Source tool I'd had no trouble like that. This time, you have to open it by yourself as if it's Open-Sourced - with a technique of reverse-engineering. Now, as I can't reverse-engineer tools made by Microsoft since it's likely to be prohibited in their licenses, I tell you how <em>you</em> can get what you need all by yourself.<br/><br/>There are a few known debuggers for their powerful reverse-engineering and system-diagnosing capability, among which I recommend <a href="http://www.ollydbg.de/" target="_blank">OllyDbg</a> as the best free tool available. Download it and unpack it, then run it. Load <code>makecert.exe</code> in OllyDbg by <code>File | Open</code>. In <code>Debug | Arguments</code>, feed OllyDbg with the argument line which should be passed into the debug target, <code>makecert.exe</code>, the argument with which you could generate a certificate and put it in a certificate store with an associated key pair. An example would look like<br/><br/><pre class="brush: cpp">-sr LocalMachine -ss MY -sk Temp000 -n CN=testcert -r</pre><br/><br/>With this argument, it'll create a new key pair and a new certificate, then will place them intertwined in the machine certificate store named 'MY'. 'MY' is the name of the personal store you can find in the certificate MMC. In my case, I wanted to use a certificate and its associated key pair from a Windows service which runs in the SYSTEM account. I had to put it in the local machine store which can be globally seen from all users who belong to the same machine. By setting the argument in OllyDbg, you are prompted to restart the debug target to apply the new argument, so restart it via <code>Debug | Restart</code>. It pauses at the entry point of the target program just like the first time you load the target.<br/><br/>Now all necessary things are laid out before your eyes; you are ready to investigate how <code>makecert.exe</code> works. While you can step through the assembly code all the way, this particular case allows you to take a shortcut. You have already seen the source code of the <code>CreateCert</code> program, which means you already know how the flow chart of the program will look like when you generate a certificate. Control points in this imaginary flow chart are most likely to be mapped to API calls in the actual <code>makecert.exe</code>.<br/><br/>OllyDbg has many convenient features for inspection into internals of a binary code. Focus the <code>CPU</code> window and open the right-click context menu, select <code>Search for | All intermodullar calls</code>. A window that shows all intermodular calls with verbose symbol names will appear. If the destination is <code>kernel32.CreateFileA</code>, it means <code>makecert.exe</code> calls the <code>CreateFileA</code> function in the <code>kernel32.dll</code> kernel module. Now browse the list of the destination functions. Among function calls to <code>kernel32</code>, <code>msvcrt</code>, <code>user32</code> and others, you'll find many calls to functions in the <code>CRYPT32</code> module. Among them there'd be ones with the 'Cert' prefix. The primary target is, as you can guess by the <code>CreateCert</code> sample program, <code>CertSetCertificateContextProperty</code>, so look for it.<br/><br/>If you can find it, what you should know next is in what context it's called in <code>makecert.exe</code>. That is, you have to know the correct arguments for the function. OllyDbg can set a breakpoint on every call to a function you choose, so highlight the <code>CertSetCertificateContextProperty</code>, right-click to open the context-menu, select the command "Set breakpoint on every call to <code>CertSetCertificateContextProperty</code>". In addition to this particular function, you may want to add breakpoints to other related functions such as <code>CertAddCertificateContextToStore</code> to manage a certificate store to get additional clues in the flow.<br/><br/>Now let's run the program to reach the place where <code>CertSetCertificateContextProperty</code> is called. Select <code>Debug | Run</code> in the OllyDbg menu. It starts execution of <code>makecert.exe</code> under the debugger and it'll stop at the first time <code>CertSetCertificateContextProperty</code> is called in the program. When it stops at a call to <code>CertSetCertificateContextProperty</code>, you are at a step of a CALL opcode to call the function. Select <code>Debug | Step Into</code> to proceed just 1 step ahead. Now you jump in the block of the function code. Well, what to do here? Of course you've come here to figure out the correct arguments for the function. Select <code>View | Call stack</code> in the menu. You have the window that shows the call stack of the thread. At the top of the stack, there should be a call to <code>CertSetCertificateContextProperty</code> and you can see what arguments are actually set in the call. According to the MSDN Library, the signature of <code>CertSetCertificateContextProperty</code> is like this:<br/><br/><pre class="brush: cpp">BOOL WINAPI CertSetCertificateContextProperty(<br/> PCCERT_CONTEXT pCertContext,<br/> DWORD dwPropId,<br/> DWORD dwFlags,<br/> const void* pvData<br/>);</pre><br/><br/>The first argument is a handle to a certificate you fiddle with. The second one, "Arg2" in a call stack window of OllyDbg, is used to specify which property of a certificate you are interested in. If it's 0x00000002, see <code>wincrypt.h</code> header file in the Platform SDK and you'll know <code>CERT_KEY_PROV_INFO_PROP_ID</code> is defined as 0x00000002. <code>CERT_KEY_PROV_INFO_PROP_ID</code> is, as its name suggests, a property to store info about a key provider. The fourth argument is where you set actual data to pass as a new property value. If Win32 API were organized in an object-oriented way it'd be far easier to manage those certificates, but in reality you have to control those objects by C functions mimicking OO in a not-so-intuitive way. Anyway, the fourth argument should be a pointer to a <code>CRYPT_KEY_PROV_INFO</code> struct. OllyDbg is nifty to track these arguments, the only thing what you have to do manually is highlight the argument in the call stack window and select <code>"Follow address in stack"</code> in the right-click context menu. The CPU window is raised up, in the right bottom of it you'll see a memory map around the struct passed to the function. The <code>CRYPT_KEY_PROV_INFO</code> struct is defined in <code>wincrypt.h</code> like this:<br/><br/><pre class="brush: cpp">typedef struct _CRYPT_KEY_PROV_INFO {<br/> LPWSTR pwszContainerName;<br/> LPWSTR pwszProvName;<br/> DWORD dwProvType;<br/> DWORD dwFlags;<br/> DWORD cProvParam;<br/> PCRYPT_KEY_PROV_PARAM rgProvParam;<br/> DWORD dwKeySpec;<br/>} CRYPT_KEY_PROV_INFO, *PCRYPT_KEY_PROV_INFO;</pre><br/><br/>The memory layout is: a 32-bit/4 bytes (if you are on Win32) pointer to a wide string for a container name, another 4-bytes pointer to a provider name string, a 4-bytes DWORD for a provider type, another 4-bytes DWORD for flags, and the rest. OllyDbg can show actual UNICODE strings pointed by those LPWSTR pointers. The container name you set in the argument for <code>makecert.exe</code> should be there. (In the aforementioned example, "Temp000".) The memory map shown in OllyDbg is stacked as 32-bit blocks. Just under the container name you can see a provider name such as "Microsoft Strong Cryptographic Provider" found in wincrypt.h as the value of <code>MS_DEF_PROV</code>. The block below the pointer to a provider name will be the value 0x00000001. It's the value of <code>PROV_RSA_FULL</code> provider type. The next block is for flags and it's what I couldn't make out only from the description in the MSDN Library.<br/><br/>The document in the MSDN Library mentions <code>CryptAcquireContext</code> flags like "See <code>CryptAcquireContext</code> for the list of flags passed through." but only lists 2 special flags on the page, and the function I intended to use was <code>CertSetCertificateContextProperty</code> and not <code>CryptAcquireContext.</code> Thus I had no idea that there are other flags. What's unlucky for me was the <code>CreateCert</code> sample code is completely lacking in this point. Basically it sets nothing in the flags and Microsoft leaves you in the cold when you try to associate a private key with a certificate.<br/><br/>Now watch what value is in the place of <code>dwFlags</code> - <code>makecert.exe</code> sets 0x00000020 in <code>dwFlags</code> of <code>CRYPT_KEY_PROV_INFO</code> struct. In <code>wincrypt.h</code>, it's the value of <code>CRYPT_MACHINE_KEYSET</code>. Yes, it's plain and simple (to the shame of me who couldn't make out it at the first glance in the MSDN Library document). The correct key container is selected by setting the value in <code>dwFlags</code> when a private key is associated with a newly created certificate. Since the <code>CreateCert</code> sample code doesn't set it, a private key is lost and you can't get a credential handle by the <code>AcquireCredentialsHandle</code> API.<br/><br/>From here on I'll show you a complete C++ sample code to programmatically generate a PKCS#12-formatted PFX file only with the Windows Platform SDK functions. When built and executed, it generates a self-signed certificate and an associated keypair and puts them in the machine certificate store.<br/><br/>First we put the necessary headers...<br/><br/><pre class="brush: cpp"><br/>// for FXExportCertStoreEx<br/>#ifndef _WIN32_WINNT<br/>#define _WIN32_WINNT 0x0500<br/>#endif<br/><br/>// some certificate functions are affinitive with UNICODE<br/>#ifndef UNICODE<br/>#define UNICODE<br/>#endif<br/><br/>// required to link this library for certificate functions<br/>#pragma comment(lib, "Crypt32.lib")<br/><br/>#include <windows.h><br/>#include <wincrypt.h><br/>#include <malloc.h><br/><br/>#include <iostream><br/>#include <fstream><br/><br/>using namespace std;<br/></pre><br/><br/>C++ headers <code>iostream</code> and <code>fstream</code> are just included for the sake of easier file access.<br/><br/>Let's proceed to the main function. This simple program has no options as they are hardcoded as variables such as a certificate subject name and a key container name. "CN" stands for <em>common name</em> in the X.500 standard.<br/><br/><pre class="brush: cpp"><br/>int main()<br/>{<br/> wchar_t* pszCertificateSubjectName = L"CN=Test Subject";<br/><br/> DWORD dwSize = 0;<br/> if (!CertStrToName(<br/> X509_ASN_ENCODING,<br/> pszCertificateSubjectName,<br/> CERT_OID_NAME_STR,<br/> NULL,<br/> NULL,<br/> &dwSize,<br/> NULL<br/> ))<br/> {<br/> cerr << "Invalid certificate subject name" << endl;<br/> return 1;<br/> }<br/><br/> PBYTE p = (PBYTE)_alloca(dwSize);<br/><br/> if (!CertStrToName(<br/> X509_ASN_ENCODING,<br/> pszCertificateSubjectName,<br/> CERT_OID_NAME_STR,<br/> NULL,<br/> p,<br/> &dwSize,<br/> NULL<br/> ))<br/> {<br/> cerr << "Invalid certificate subject name" << endl;<br/> return 1;<br/> }<br/><br/></pre><br/><br/>This apparently redundant operation is typical for crypto functions. You have to encode a plain string into a crypto-friendly format by youself to pass it to a crypt function. Memory management is all up to you, so these APIs don't have convenient features such as automatic conversion. Since the correct size of an encoded string is not known beforehand, you have to call a conversion API to know the final size, allocate a memory, then set it as the destination of a conversion.<br/><br/>Here we prepare a key container in the RSA cryptographic service provider for generation and verification of a key-pair and a certificate by the RSA public key algorithm which is now patent-free.<br/><br/><pre class="brush: cpp"><br/> CERT_NAME_BLOB sib;<br/> sib.cbData = dwSize;<br/> sib.pbData = p;<br/><br/> wchar_t* pszKeyContainerName = L"Test Container Name";<br/><br/> HCRYPTPROV hProv = NULL;<br/> if (!CryptAcquireContext(<br/> &hProv,<br/> pszKeyContainerName,<br/> MS_DEF_PROV,<br/> PROV_RSA_FULL,<br/> CRYPT_NEWKEYSET | CRYPT_MACHINE_KEYSET<br/> ))<br/> {<br/> if (GetLastError() == NTE_EXISTS)<br/> {<br/> if (!CryptAcquireContext(<br/> &hProv,<br/> pszKeyContainerName,<br/> MS_DEF_PROV,<br/> PROV_RSA_FULL,<br/> CRYPT_MACHINE_KEYSET<br/> ))<br/> {<br/> cerr << "Can't get a crypto provider" << endl;<br/> return 1;<br/> }<br/> }<br/> }<br/></pre><br/><br/>Next, you have to open the "MY" certificate store in the local machine. The "MY" certificate store is where personal certificates are stored. Then, if it contains a certificate previously generated by this program, the previous one is deleted.<br/><br/><pre class="brush: cpp"><br/> HANDLE hCertStore = CertOpenStore(<br/> CERT_STORE_PROV_SYSTEM,<br/> X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,<br/> hProv,<br/> CERT_SYSTEM_STORE_LOCAL_MACHINE<br/> | CERT_STORE_NO_CRYPT_RELEASE_FLAG<br/> | CERT_STORE_OPEN_EXISTING_FLAG,<br/> L"MY"<br/> );<br/> if (!hCertStore)<br/> {<br/> CryptReleaseContext(hProv, 0);<br/> cerr << "Can't open the \"MY\" certificate store in this local machine" << endl;<br/> return 1;<br/> }<br/><br/> PCCERT_CONTEXT pCertContext = CertFindCertificateInStore(<br/> hCertStore,<br/> X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,<br/> 0,<br/> CERT_FIND_SUBJECT_NAME,<br/> &sib,<br/> NULL<br/> );<br/> if (pCertContext)<br/> {<br/> cerr << "Found the certificate - deleting it" << endl;<br/> if (!CertDeleteCertificateFromStore(pCertContext))<br/> {<br/> CryptReleaseContext(hProv, 0);<br/> cerr << "Can't remove the old certificate" << endl;<br/> return 1;<br/> }<br/><br/> pCertContext = NULL;<br/> }<br/></pre><br/><br/>Now that the certificate store is ready, let's generate a new key pair for key exchange in the key container by the <code>CryptGenKey</code> function with <code>AT_KEYEXCHANGE</code> as it's used for SSL encrypted communication. Then, set up a struct with properties you've prepared so far and call <code>CertCreateSelfSignCertificate</code> with it to create a self-signed certificate.<br/><br/><pre class="brush: cpp"><br/> HCRYPTKEY hKey;<br/> if (!CryptGenKey(hProv, AT_KEYEXCHANGE, CRYPT_EXPORTABLE, &hKey))<br/> {<br/> CryptReleaseContext(hProv, 0);<br/> cerr << "Can't generate a key pair" << endl;<br/> return 1;<br/> }<br/><br/> CRYPT_KEY_PROV_INFO kpi;<br/> ZeroMemory(&kpi, sizeof(kpi));<br/> kpi.pwszContainerName = pszKeyContainerName;<br/> kpi.pwszProvName = MS_DEF_PROV;<br/> kpi.dwProvType = PROV_RSA_FULL;<br/> kpi.dwFlags = CERT_SET_KEY_CONTEXT_PROP_ID;<br/> kpi.dwKeySpec = AT_KEYEXCHANGE;<br/><br/> SYSTEMTIME et;<br/> GetSystemTime(&et);<br/> et.wYear += 10;<br/><br/> CERT_EXTENSIONS exts;<br/> ZeroMemory(&exts, sizeof(exts));<br/><br/> PCCERT_CONTEXT pc = CertCreateSelfSignCertificate(<br/> hProv,<br/> &sib,<br/> 0,<br/> &kpi,<br/> NULL,<br/> NULL,<br/> &et,<br/> &exts<br/> );<br/> if (!pc)<br/> {<br/> CryptDestroyKey(hKey);<br/> CryptReleaseContext(hProv, 0);<br/> cerr << "Can't create a self-signed certificate" << endl;<br/> return 1;<br/> }<br/></pre><br/><br/>A newly created certificate comes as a certificate context. You have to put it in the certificate store prepared in the previous steps.<br/><br/><pre class="brush: cpp"><br/> if (!CertAddCertificateContextToStore(<br/> hCertStore,<br/> pc,<br/> CERT_STORE_ADD_REPLACE_EXISTING,<br/> &pCertContext<br/> ))<br/> {<br/> CryptDestroyKey(hKey);<br/> CertFreeCertificateContext(pc);<br/> CryptReleaseContext(hProv, 0);<br/> cerr << "Can't create a self-signed certificate" << endl;<br/> return 1;<br/> }<br/><br/> CertFreeCertificateContext(pc);<br/></pre><br/><br/>...and this is the most important point of this whole article. It shows how you associate a private key with a certificate in a certificate store. Note that it gives <code>pCertContext,</code> instead of <code>pc</code>, to the <code>CertSetCertificateContextProperty</code> function. <code>pCertContext</code> is the certificate context you get as a handle to the certificate you just put in the certificate store. Since it's a handle stored in the certificate store, any operation to the handle affects the certificate in the store. By setting <code>CRYPT_MACHINE_KEYSET</code> in the flags, it searches a correct place for the key container and sets a new property value in it.<br/><br/><pre class="brush: cpp"><br/> CRYPT_KEY_PROV_INFO ckp;<br/> ZeroMemory(&ckp, sizeof(ckp));<br/> ckp.pwszContainerName = pszKeyContainerName;<br/> ckp.pwszProvName = MS_DEF_PROV;<br/> ckp.dwProvType = PROV_RSA_FULL;<br/> ckp.dwFlags = CRYPT_MACHINE_KEYSET;<br/> ckp.dwKeySpec = AT_KEYEXCHANGE;<br/><br/> if (!CertSetCertificateContextProperty(<br/> pCertContext,<br/> CERT_KEY_PROV_INFO_PROP_ID,<br/> 0,<br/> &ckp<br/> ))<br/> {<br/> CertFreeCertificateContext(pCertContext);<br/> pCertContext = NULL;<br/> CryptDestroyKey(hKey);<br/> CryptReleaseContext(hProv, 0);<br/> cerr << "Can't set certificate property" << endl;<br/> return 1;<br/> }<br/></pre><br/><br/>The certificate and the key pair are ready in the store, so let's try to export them in one as a PKCS#12 PFX file. For this operation the platform SDK has very straight PFX-managing functions so you won't have any trouble. All you have to do is to save a PFX binary blob as a file.<br/><br/><pre class="brush: cpp"><br/> CRYPT_DATA_BLOB cdb;<br/> ZeroMemory(&cdb, sizeof(cdb));<br/><br/> wchar_t* pszPassword = L"Test Password";<br/><br/> if (!PFXExportCertStoreEx(<br/> hCertStore,<br/> &cdb,<br/> pszPassword,<br/> NULL,<br/> EXPORT_PRIVATE_KEYS<br/> ) || cdb.cbData == 0)<br/> {<br/> CertFreeCertificateContext(pCertContext);<br/> pCertContext = NULL;<br/> CryptDestroyKey(hKey);<br/> CryptReleaseContext(hProv, 0);<br/> cerr << "Can't export a certificate from the certificate store" << endl;<br/> return 1;<br/> }<br/><br/> cdb.pbData = (PBYTE)_alloca(cdb.cbData);<br/><br/> if (!PFXExportCertStoreEx(<br/> hCertStore,<br/> &cdb,<br/> pszPassword,<br/> NULL,<br/> EXPORT_PRIVATE_KEYS<br/> ) || cdb.cbData == 0)<br/> {<br/> CertFreeCertificateContext(pCertContext);<br/> pCertContext = NULL;<br/> CryptDestroyKey(hKey);<br/> CryptReleaseContext(hProv, 0);<br/> cerr << "Can't export a certificate from the certificate store" << endl;<br/> return 1;<br/> }<br/><br/> {<br/> ofstream out("test.pfx", ios::binary);<br/> if (!out)<br/> {<br/> CertFreeCertificateContext(pCertContext);<br/> pCertContext = NULL;<br/> CryptDestroyKey(hKey);<br/> CryptReleaseContext(hProv, 0);<br/> cerr << "Can't write a pfx blob" << endl;<br/> return 1;<br/> }<br/><br/> out.write((char*)cdb.pbData, cdb.cbData);<br/><br/> if (!out)<br/> {<br/> CertFreeCertificateContext(pCertContext);<br/> pCertContext = NULL;<br/> CryptDestroyKey(hKey);<br/> CryptReleaseContext(hProv, 0);<br/> cerr << "Can't write a pfx blob" << endl;<br/> return 1;<br/> }<br/> }<br/><br/> CertFreeCertificateContext(pCertContext);<br/> pCertContext = NULL;<br/> CryptDestroyKey(hKey);<br/> CryptReleaseContext(hProv, 0);<br/><br/> cout << "Done" << endl;<br/><br/> return 0;<br/>}</pre><br/><br/>Since Microsoft pushs .NET Framework for daily work these days, those complicated native Windows APIs are not as often employed as before. But still the fact that some arcane cryptographic functions can't find their peers in .NET Framework forces you to use APIs in the Platform SDK if necessary. Performance is another reason not to rely on .NET Framework where some methods are internally implemented in Managed code as seen in Rijndael encryption in the System.Security.Cryptography namespace. If Open Source libraries are available you are free to use them, but using Platform SDK can suppress bloat in executable size. It's a trade-off after all, however you have no choice sometimes, just to the degree where you have to resort to things like reverse-engineering.久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-19731141074752099662004-04-28T15:53:00.000+09:002016-02-07T02:13:35.382+09:00DICE version 0.84 配布昨年末に<a href="http://www.princeofpersiagame.com/uk/" target="_blank">Prince of Percia: SOT</a>をやって以来、全くPCゲームをプレイする気が失せていたのは前回書いたとおりである。<a href="http://www.painkillergame.com/" target="_blank"> Painkiller</a>も昔の面クリア型ゲームを中途半端に模したような感じでかなり興醒めというか期待外れだった。ところが、 ビデオカードのアップグレード予定も当分ないので今年はこのまま何もやらずに済まそうかなどと思っていたところ、あっさりと その方針を覆らせるゲームに当たってしまった。何のことはない、<a href="http://www.unrealtournament.com/ut2004/" target="_blank">Unreal Tournament 2004</a>である。デモ版をダウンロードして遊んでみたところ、ネットワーク対戦のOn-slaughtが実に面白い。デモ版でこれだけ遊べていいのだろうかという出来の良さに率直に驚いてしまった。<br />
<br />
前作では、前々作からのグラフィックスの進歩にばかり目を取られ、時間もないということでUTで定評のあるボットの能力を信用してシングルプレイのラダーを クリアしあっさり通り過ぎていたのが、今作のONSで遅ればせながらやっとネットワーク対戦FPSの真価に触れたような気がする。グラフィックス上は前作からほとんど進歩しておらず、またキャラクターもほとんど使い回しという状態ではあっても、乗り物 が正式サポートされたことで遊びの幅が立体的に広がった。最近では乗り物が登場するFPSは珍しくないとはいえ、Unrealの世界の武器の扱いに慣れすぎているせいか、Halo等では味わえない絶妙のバランスを感じる。それも、Haloほどにはかっちりと した作りでもなく、Unrealという名の文字通り非現実的にキャラクターがぴょんぴょん跳び回りつつ激しく接近戦を行うというのがUnreal Tournamentの真骨頂だ。20分程度で済むONSは実にリプレイバリューが高く、たとえ負けても激しい抗戦ができればそれなりに満足感がある。その場限りの人間のチームは、上級者と下級者が混ざっているのでよりメリハリのあるプレイを楽しめる。ONS(とASの一部)では乗り物もあるので、1on1だとさかんにサイドステップで飛び回る上級プレイヤーを叩ける逆転のチャンスも増え、集団ゲームとして実に合理的なハンディキャップ配置である。PCのいわゆる洋ゲーしかやらず、UTではなくUnreal本編のようなシングルプレイヤー物を好み、一回最後までやったゲームは大概そのまま手放してしまう私のような人間にとって、この中毒性の陶酔は久々の感覚である。下手をすると一年間UT2004だけでゲームの欲求をまかなえそうな気すらしている。良作の太鼓判を押したい。<br />
<a name='more'></a><br />
ただ、これだけ褒めちぎり甲斐のあるUT2004でも、弱点はある。それはシングルプレイにおけるボットのAIである。定評のあるUTのボットだが、今回ばかりは人間と対比しての部分で分が悪い。人間相手の対戦ばかりやっていると、ボットの挙動の機械的な限界と、人間のプレイヤーの人間的な癖や戦略的強さとの差異を、画面越しにひしひしと感じる。例えば、シングルプレイのラダーにONSは入っていないのでINSTANT ACTIONでONSをプレイしてみると、ボットは非常に合理的にリンクノードを攻めようとするか、あるいは異常に向こう見ずな先制特攻をかけてくるかの二つに一つである。ボットの技能を調節したときは、シングルプレイのラダーを上っていけば分かるよう に、チームプレイヤーの4つの能力パラメータ(正確さ/攻撃性/敏捷さ/戦略)がきちんと上昇していき、Skaarjの場合サイド ステップが多いなどのゲーム上の特徴を除くとほとんどむらがない。そして、上級ボットが恐ろしく武器の持ち替えが早いとか反応速度が速いとかいった形で強いのに対し、低級ボットは妙に動作が遅く照準も定まらないといったようなわかりやすい差が、 ONSに比べゲームの密度の濃い1on1の戦闘ですら時折気になる。ところが、人間の上級プレイヤーの場合は、抽象的な表現をす れば、反射ではなく先読みが異常に優れている。それは、人間のプレイヤーの癖を学習しないと得られない戦略であり、残念ながら製品版でもそういった観点のボット強化はあまり行われなかったようである。結果的に、ONSのシングルプレイは非常に味気ないものになっている。もちろん裏を返せばそれだけネットワーク対戦が魅力的であるということになる。<br />
<br />
具体的な人間プレイヤーの挙動としては、ネットワーク対戦を観察していて最も人間的に感じたのは、Mantaで相手の頭上高く跳ねてそのままほぼ垂直降下し相手を潰すテクニックである。真上という、マウスで回す場合視界が最も及びにくいと思われる方向から攻撃を仕掛け人間のプレイヤーが最も嫌う死角を突くというこの戦略は、ボットにあらかじめ組み込めば可能なのだろうが、それにしても実に人間的である。車のクラクションを鳴らして味方を拾うのも些細なことだが協力プレイの基本コミュニケーションだ。またマップの特性を理解した上で、リンクノードを移動せずに超長距離から乗り物で砲撃するのもボットにはない人間プレイヤーの常套手段だろう。こうしてみると、相手の人間的な癖を読んだ戦略の効用を自分も理解できゲーム内の世界で学習していくという感覚を覚えた ときこそが、ネットワークFPSの真髄を直感する瞬間と言えはしないだろうか。これは、同じネットワークゲームでも、MMORPGの世界とはおそらく異質な、あるいはもっと鮮烈な体験である。会話は罵声と"pwnage""gg"等々以外ほとんど欠くネットワークFPSでは、侍の世界よろしく、プレイヤーが無言の対話を丁々発止の決闘の最中に交わしているのだ。 そうはいっても負けたときにはそれなりに腹立たしさも感じるのでついつい勝つまで繰り返すこともある。それでも、ボットのチームに理不尽な負け方をした時ほどの屈辱感はないのが不思議だ。近い将来もっと賢いAIがボットに搭載され人間プレイヤーとの区別が付かない程度まで進歩するとしたら、この感覚がどのように変容するのか、非常に興味深い。<br />
<br />
今回のバージョン0.84からDICEのHTTPサーバ部分に、.NETアプリケーションコンテナと称して、.NETオブジェクトをホ スト出来 るようにした。C#/VB.NET/JScriptのコードをアセンブリにコンパイルして実行する機能と、.NETアセンブリを直接実行する機能がある(ただしエントリポイントはMainではなくて特定の抽象クラスをユーザに実装してもらう方式)。リース期間はデフォルトで無限なので、CGIとは違 いISAPIのようにメモリ上に置きっぱなしになり、PHPやJ2EEのようなセッション管理も可能である(ユーザがコードを書きさえすれば)。ただしメ モリはそれなりに食うようである。ひょっとしたらリークしているのかもしれないが、CLRだかCOMマーシャリングだかのキャッシュはある程度呼び出しを繰り返すと落ち着くようだ。折角のWebサーバ機能にも関わらずCGIのようなダイナミックコンテンツをまったくサポートしないのはさすがに格好悪いよねということで色々考えていたところ、一番簡単そうなのがこれということで.NETを対象にすることになった。本当はASP.NETがサポートできれば大した話なんだろうが、さすがにそこまで大仰な物は作れないので、自分でC#やVB.NETのコードを書いたりコンポーネントを利用したりする人がプログラマ ブルに使えればいいだろうということでこの形にした。現在のASP.NETや、将来のYukonのストアドプロシージャ同様に、Unmanagedコード 内にCLRをホストし、そこで.NETのオブジェクトを動かすという仕組みである。今のところセキュリティもへったくれもないのでサーバ管理者自身が好き放題に書いたスクリプトを実行するという使い方を想定している。いずれにせよWindows XP SP2でHTTP.SYSが搭載されるとのことなのでパフォーマンスの点ではIISに対抗しても仕方ない。Perl、PHP、Rubyなどがサポートできれば既存のCGIスクリプトの再利用という観点からは最良なのだけれど、PerlはActiveStateのdllが関係しそうだしPHPも Rubyも個人的に興味ないしで(CGIが使っている標準出力とかフックするのは大変そうだしRubyの組み込みマニュアルは本当にスカスカだし)、かと いってApacheモジュールを使えるようにするとかいった計画は遊びには面倒すぎる。そこで.NETアプリケーションサーバという名目で行くことになった。世に出ているJavaのアプリケーションサーバは大概自前のJavaの実装を持っているようだけれど.NET系のものはどうなっているのだろうか。<br />
<br />
一応DICEは今となってはやや古いWindows 2000も対象としているので、あまり.NET Frameworkには触れずに済ませ、次に作るアプリケーションで色々新しい構成を考えようと従来思っていたけれども、ある問題についてなかなか良い解がないので思いあぐね、DICEの方に一つの関連する方法を実装してみて具合を見ようというのも今回の追加機能の目的の一つである。その問題というのは、UnmanagedアプリケーションとManagedアセンブリの通信チャネルの選択である。それも、双方向で非同期の通信を可能にしたい。今回の場合で 言うと、.NETコンポーネント側から見るとUnmanagedのDICEから非同期に呼び出しが来るように見えても、ホストされている.NETコンポー ネントが非同期にDICEを呼び出す手段は提供していない。今のところ、DICEがホストしている.NETオブジェクトから非同期にDICEを呼ぶには、DICEにCOMのインターフェイスをつけて登録するのが簡単そうではある(既にATLサービスなのでレジストリも使っている)が、どうもスマートでない気がする。大体、ホストしている.NETコンポーネントの方へデバッガを仕掛けることがどうやら現状のDICEでは不可能なので、テストユニットを使ってドライバを用意したりする暇もない状況ではかなり面倒である。最近VC++ 7.1が無料配布されることになったが、やはりVCの肝はデバッガで、それが抜けていると少なくとも開発には役に立たない。Unmanaged/Managedの一番大袈裟な通信方法はおそらくMSMQやCOM+を使うものでこれはプラットフォームに依存しすぎる嫌いがある。そこで、以前は、TCP/IP接続が、低レベルながら移植性が高い上に位置透過性もあり最善と考えていたが、プロトコルを一々実装するのはやはり面倒は面倒だ。<br />
<br />
最近は、上述のデバッガ問題から、Unmanaged C++にしがみつくのはやめにして、Managed C++でラッパーを作ってアプリケーション自体はいい加減に.NETの方へ重点を移し、位置透過性は.NETリモーティングでどうにかするのがいいかもと考えている。それなら、さすがにデバッガも問題なく使えるはずだ。そのためには、今使っているC/C++のライブラリをどうにかする必要が出てきそうで、 これもなかなか頭が痛い。DICEのために作ったフレームワークも、次にそのまま使うというわけにはいかない。とはいっても、もう私の中では腹は決まって いる。今回のDICEのようにC++で作った部分はピュアなUnmanaged実行ファイルとして保ち分離されたManagedアセンブリと通信させるモデルは、個人の立場ではこれ以上の維持は無理だというのが私の結論である。それよりは、パフォーマンスは要求しないアプリケーションの動作部分やGUIはC#で書き、パフォーマンスを要求する部分やレガシーコードをUnmanaged C/C++のコードをManaged C++でラップして、それらを一つのアプリケーションとして綴じ込むべきで、同期だの非同期だのといったことはマシン内では忘れた方が良い。実に単純な話 だが、C++テンプレートのテクニックやパフォーマンスハックなどC++の楽しさに拘泥しすぎて完全な宗旨替えがいささか遅れてしまったようである。<br />
<br />
DICEで一番デバッグに時間がかかったのはパースレッドメモリアロケータの部分だが、皮肉にも、それに次いで長い時間を<a href="http://stlport.sourceforge.net/" target="_blank">STLport</a>の バグによって起こる、デバッガで追い切れない原因不明のメモリ破壊などの問題で失った。VC++ 7.1対応とバグ回避のためにCVSスナップショットを使用し始めたところ、そこに別のバグがあり、貴重な時間を随分失うことになってしまった。しかも更新 の度に別の新しいバグをコミットしてくれたりするので参った。最近になってCVS内のテストコードが充実し安定してきたからよかったものの、0.83のリ リースから0.84まで5か月近くもかかってしまったのは<a href="http://stlport.sourceforge.net/" target="_blank">STLport</a>のCVSの更新を待っていたのが主な原因である。0.83は実は重要な問題を抱えていたにもかかわらず、足踏みする羽目になった。DICE 0.83使用者には、早期の入れ替えをお勧めする。.NETアプリケーションコンテナは、その期間の開きに対して埋め合わせとして、また上記の実験のため に入れたまでだ。これがパフォーマンスクリティカルな部分のみUnmanagedコードで、かつアプリケーションの他ライブラリ依存がもっと低ければ、こ んな苦労をせずに済んだことだろうと思う。<a href="http://stlport.sourceforge.net/" target="_blank">STLport</a>がオープンソースなのは不幸中の幸いである。.NETの場合で不安なのはマイクロソフトのライブラリにバグがあっても修正がなかなか得られないことくらいだ。ネイティブコードの世界ですら、最近<a href="http://www.digitalmars.com/d/" target="_blank">プログラミング言語D</a>が 以前より大きな注目を集めるなど、確実に流れは新しい次元へ、それも以前よりさらに速く向かいつつある。.NETは登場以降3年経って全然普及していない などと言われるが、Longhornを前にして、ギアは一段上がった。コミュニティとレガシーコード群の大きさを除けば、C/C++は、そこでは明確に不利な、格下の存在だ。よくても、UnmanagedとManagedを繋ぐグルーコードの扱いである。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-37804460089146162682004-01-10T16:49:00.000+09:002016-02-07T02:11:18.670+09:00DICE version 0.83 配布2003年の年頭には下記のように6回はこのページを更新するなどとうそぶいてみたものの蓋を開けてみると4回しか更新していなかったりする。しかも、すぐ下ではプログラミングに邁進するかのようなことが書かれているが目下全く手が進んでいないのでこのサイト的には情けない限りである。年末に当年の総括をするのが一般的なようだが、12月も1年のうちには違いないので、自己の1年を顧みての反省は1月にするのが適当である。<br />
<br />
反省と言えば、犬も歩けば blogにあたるというほどbloggingがWebで流行っている昨今、独り書きあるいは自省という行為が広まったようにも見える。しかし、実際の所blogというものは暗黙に、あるいは明示的に、対話や双方向の関連づけを前提として作られており、主に自省のためというより、他者が常に意識されている活動である。Webで公開されている以上当然ではないかという意見もあろうが、blog以前に昔の日本のWebで行われていたWeb日記というジャンルは、多 くの場合匿名で行われていたので、個人が他者とのコミュニケーションのフィードバックを求めるという側面を必ずしも持っていなかった。いわば、一方的な告白である。当時はWeb日記などには全然関心が無かったが、昨今blogに当たらずに過ごす方が無理なので毎日毎日よく時間を費やして書けるもんだと私などは感心して色々見て回っている。<br />
<br />
Webデザインについても、blogの場合なぜか例外なく「シンプルで趣味のいいデザイン」を目指したと思しき様式に統一されているのも面白い。写真もそんなシンプルなフレームの間に挟まれていると実に映える。私は去年初めに<a href="http://www.mozilla.org/products/firebird/" target="_blank">Mozilla Firebird</a>を使い始めるまでNN4に固執していた関係でスタイルシートやJavaScriptに関心を持つのが遅れたので未だにその辺りについて何か学習するたびに面白みを覚えてしまう。DICEのWebインタフェイスを作るときにMozilla/IEのDOM操作で面白いことが色々出来るのにも開眼した。しかしこれがFlashまで行くと奥ゆかしさを失うような気がするのは何故だろう。あくまでWebブラウザonlyで出来るのが好ましいと感じる。<br />
<a name='more'></a><br />
blogのなかで私が一番頻繁に見ているのは<a href="http://www.ruby-lang.org/ja/" target="_blank">Ruby</a>開発者の松本行弘の<a href="http://www.rubyist.net/~matz/" target="_blank">blog</a>である。プログラミングに関心のある人間なら楽しんで読める内容がほぼ毎日書かれているのは驚嘆に値する。<a href="http://www.ruby-lang.org/ja/" target="_blank">Ruby</a>に特に関心のない私がそう感じるのだから間違いない(Ruby 2という奴は気になるけど)。もっとも、当の<a href="http://www.rubyist.net/~matz/" target="_blank">blog</a>で もオープンソース技術者というものは広告塔(エヴァンジェリストと言う奴か?)になるべきか否かという議論が、blog自体が明らかに広告であるにもかかわらず訪問者によって交わされるところを見ると、「日本の文化はつくづく奥ゆかしい」と好ましくも疎ましくも感じる。他に、プログラミング関係では、<a href="http://www.pluralsight.com/blogs/dbox/default.aspx" target="_blank">Don Boxのblog</a>もLonghornオフレコ談義ばかりで読み甲斐がある。笑えたのはC++ Primerなどを書いている<a href="http://blogs.msdn.com/slippman/" target="_blank">Stanley Lippmanのblog</a>で、まるっきりC++.NETの新仕様のプレゼンテーションとして使われている。こうした、普段それほど散文を書くとは思えない人々の文章を眺めるのはそれ自体楽しいが、一方で、文筆業の人間こそ読み甲斐のあるlogを書くはずである。書くはずであると思いきや、例えば<a href="http://www.williamgibsonbooks.com/blog/blog.asp" target="_blank">William Gibsonのblog</a>は去年秋に本人の執筆活動との干渉を避けるために休止されている。休止自体はなるほど彼の作品の読者に対して誠実ではあるが、オープンソースという形でのプログラムコードのインフレが現実となっている一方で、古典的な文筆業に対するインターネット上での文章のインフレは何をもたらしつつあるのだろうか。<br />
<br />
一応前回以降もPCゲームをやりましたがPCゲームの衰退をはっきりと感じさせる嫌な流れだった。プレイしたのは以下:<br />
<br />
<a href="http://www.rockstargames.com/maxpayne2/" target="_blank">Max Payne 2</a><br />
<a href="http://www.microsoft.com/games/halo/" target="_blank">Halo PC</a><br />
<a href="http://www.tron20.net/" target="_blank">TRON 2.0</a><br />
<a href="http://www.microsoft.com/games/dungeonsiegeloa/" target="_blank"> Dungeon Siege: Legends of Aranna</a><br />
<a href="http://www.princeofpersiagame.com/uk/" target="_blank">Prince of Persia: The Sands of Time</a><br />
<a href="http://www.republictherevolution.com/" target="_blank">Republic: The Revolution</a><br />
<br />
DS: LoAは手堅い作品だが前作のエンジンの完全な使い回しで話も一本道であり、終盤かなりだれてしまったのが惜しい。とはいえDungeon Siege 2にはかなり期待している。Republicは、ものすごく期待していたタイトルだったものの、蓋を開けてみると全然思い描いたのと違っており参った。東欧の雰囲気が出ていたのは良かったにせよ、ボードゲーム + カードゲーム + 3Dはおまけというようなゲーム内容だとは全然思わなかったので、騙されたという気分が結果的に勝ってしまった。後半の単調さと終盤の破綻したゲームバランスも気にな る。PoP: TSoTは、久々にスーパーマリオのようなプラットフォームアクションゲーム(よりパズル寄りだが)をプレイする機会で結構新鮮だった。ただ、FPSに慣 れた身にとっては、頻繁に入る自動視点補正が眼精疲労を促進してくれる。また、ATIのRadeonを載せたマシンとnVIDIAのGeForceを載せ たマシンの双方でプレイしてみたところ、Radeonのマシンでは最新ドライバでも水のエフェクトが異常で、nVIDIA - the way it's meant to be played のロゴはさすが伊達ではないなと感じた。最近のベンチマークはnVIDIAの衰退を示しているが、個人的にはnVIDIAの方を応援したいところである。 TRON 2.0は音楽が結構気に入った。ビジュアルは悪くない。グリッドレースは元の映画に全然郷愁を覚えることのできない者としての自分にとっては、はっきり言って面白くも何ともなかった。Halo PCは、待ち望んだタイトルだけあってなかなか楽しめた。ただ、シーンの繰り返しが多いのと、エンジンが全く最適化されていないのには閉口した。テクスチャの圧縮をする前にPCならではの良さを追求してほしかった。そして、Max Payne 2については、正直なところ失望した。期待が大きすぎたせいかもしれないけれども、あっけないほど短いゲーム、前作のそれに毛の生えたようなラストバトル、全く目新しさが感じられないゲームデザイン、などなど明らかに前作より退化した印象を覚えた。その辺は完全にHaloの件も合わせXboxに非があると思う。もう一つ悲しいのは、<a href="http://www.deusex.com/" target="_blank">Deus Ex 2</a> (Invisible War)で、これもコンソールからの逆移植のような形態らしい。正月にやろうと思って楽しみにしていたものの、公式フォーラムでの批評があまりにもひどいので全くやる気が失せてしまった。最適化されていないエンジン、短いゲーム、などなど予想の範疇の問題が指摘されているので、やる前から気力が萎えてしま い、結局ゲームそれ自体への関心が今はここ数年で最低の状態である。果たして、救世主は現れるのだろうか? Doom 3、途上のソースコード(結構普通のC++コードで一部オブジェクトコードのままだった 笑)とコンテンツがリークしたHL2、UT 2004、それから<a href="http://www.painkillergame.com/" target="_blank">Painkiller</a>、<a href="http://www.stalker-game.com/" target="_blank">S.T.A.L.K.E.R.</a>に期待(全部FPSなんだけどね)。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-74390735395029172702003-08-16T17:15:00.000+09:002015-02-26T02:48:54.588+09:00Template Metaprogramming For Dummiesテンプレートメタプログラミングと呼ばれるテクニックがC++のコードに意識的に用いられ始めたのは1995年頃にまで遡るらしい(C++ による科学計算のためのライブラリ<a href="#R1">Blitz++</a>を作った<a href="#R2">T. Veldhuizenの記事</a>)。Alexander StepanovがテンプレートをANSI/ISOのC++委員会に提案したのがそれを遡ること2年前、1993年である。科学計算の分野で最も高速に動作するプログラムを構成するにはC言語ではなくFortranが最適であるという周知の事実に対し、C++言語でテンプレートを積極的に用いて、Fortranによる場合と同水準のパフォーマンスと、より見通しの利くオブジェクト指向を採用したプログラム構造とを両立させようという試みのもとに、このテクニックは開発されてきた。広く一般の認知を受けたのは、2001年のAndrei Alexandrescuによる著作<a href="#R3">Modern C++ Design</a>によってである。以来、<a href="#R3">Modern C++ Design</a>で解説されたジェネリックプログラミングとテンプレートメタプログラミングのテクニックを収めたライブラリ<a href="#R4">Loki</a>、<a href="#R5">Boost</a>ライブラリに収められているテンプレートメタプログラミングのためのフレームワーク<a href="#R6">MPL</a>を通じて、その利用は徐々に広がりつつある。<br/><a name='more'></a><br/>ところが一方で、このテンプレートメタプログラミングなるものが根底は非常に単純であるにもかかわらず、テンプレートについての解説すら省いてあることもあるC++の入門書などでは全く扱われていない。テンプレートメタプログラミングやジェネリックプログラミングが現れるのは、C++プログラミングに関する書籍の分類ではいわゆる上級の部類とされる書籍である。<br/><br/>私はその結びつきには全く必然性がないと考える。むしろ、C言語の解説でリンクリストがポインタを使用した基本的なツールとして与えら れるように、テンプレートメタプログラミングもテンプレートを利用した簡便でプラグマティックなツールとして紹介されてよいはずである。ひょっとしたら、テンプレートの項が継承の項より先に出てくるというラディカルな構成の教科書があっても良いかもしれない。オブジェ クト指向とジェネリックプログラミングは直交している。もっとも、テンプレートメタプログラミングは、C++テンプレートの使用から得られる直接の効果であるコードやパターンの再利用(オブジェクト指向でのクラス再利用ではない)とは若干位相を異にし、プログラム自体のパフォーマンスや最適化を追及するためのテクニックでもあるので、単にプログラミング上の生産性を上げるという目的からは逸れた、二次的なトピックとされる可能性があるのも致し方ないところ ではある。また、C++言語の解説書というのは、往々にしてC言語の熟練者を対象にしており、あまりにも異なるパラダイムのプログラミングは解説書レベルでは導入しにくいという見方もあるだろう。しかし、テンプレートをインテリジェントなマクロとして見れば、C言語の熟練者が扱いに困るとは思えない。<br/><br/>ここでは、C++におけるテンプレートメタプログラミングの、一般のオブジェクト指向プログラミングとは異質であるイディオムを、単な るおおまかなパターンの解説に留まらず基礎から列挙することによって、具体的かつ機械的にこのテクニックを習得することを目指す。オブジェクト指向は(直接には)関係ないので、C++言語の一応は知っているがオブジェクト指向は深く知らないという状態でも本文の理解には全く差し支えない。<br/><br/>C++という言語はある部分で低水準性を引きずっているため、より新しいオブジェクト指向言語(たとえばRuby)がしばしば活用されるような、具象を離れて抽象的なデザインを行う局面で用いるには泥臭すぎるとの評もあろう。しかし一方で、底辺により近いツールキットとして繰り返し用いられるべき真に再利用性の高い部品を構築するには、細部にわたって緻密な機械寄りの視点と実装が必要となるに違いない。C++があくまで開かれた言語であ るのと同様に、C++におけるジェネリックプログラミングとテンプレートメタプログラミングは、そうした細部への着目と、大胆な抽象性とを併せ持った(<a href="#R7">Multi-Paradigm</a>)、希有かつ野心的な分野であり作法である。<br/><br/>まずは、テンプレートメタプログラミングの定義から始めよう。これは、言葉の文字通りの解釈で足りる。つまり、テンプレートを使って、プロ グラムを生産するプログラム(メタプログラム)を作る、という作業を指す。ではそのメタプログラムはいつどこで実行するのかといえば、C++テンプレートについてはコンパイル時にC++コンパイラ上で実行するということになる。<br/><br/>つぎに、テンプレートメタプログラミングで何が出来るのか、何を行えばよいのかという点が問題となる。単刀直入に言えば、「テンプレートメタプログラミングで出来ること/すべきこと」とは、「C++コンパイラが出来ること/すべきこと」に他ならない。そこで、C++コンパイラがC++で書かれたコードに対しコンパイル時にCPUの種類に関係なく出来ることを列挙してみることにする。「テンプレートの展開」はC++を扱う以上当然行われる。その上で、いわゆるコンパイラによる最適化の観点からは、定数の畳み込み、定数伝播、ループ展開といった関連トピックが出てくる。C++での一般的なテンプレートの使用はパターン再利用によるコード記述量減少という効果を持つ一方で、 テンプレートメタプログラミングにおけるテンプレートは主に最適化を目的としているため、コンパイラの果たす/果たすべき機能のエミュレーションをいかに コード上で行うかという視点が有効なのは当然である。ただしアセンブリ命令を明示的に指定するような類の手作業での最適化と異なるのは、テンプレート設計 のみを行い、ソースコード生成(テンプレートのインスタンス化)の作業はコンパイラに委託する点である。<br/><br/>以下では「定数の畳み込み」「テンプレートの展開」「ループの展開」について扱う。<br/><br/>C++の主要な単位であるclassの中で定数を定義する方法は、2種類有る。ひとつはenumを用いる方法、もうひとつはstatic const変数を使用する方法である。<br/><br/><pre class="brush: cpp">struct C<br/>{<br/> enum {value = 1};<br/>};<br/><br/>struct CC<br/>{<br/> static const value = 1;<br/>};<br/></pre><br/><br/>ここでenumについて、この定数valueはC::valueのようにスコープ演算子を用いてクラスからアクセス可能になる。つまり インスタンスを生成せずにこのクラスCは用いられる。インスタンスを生成すれば'.'(ドット)演算子によってもアクセス可能だが実益はない。const 変数については、同様にクラスからアクセスするためと、コンストラクタで初期化できるただのconst変数では意味がないので、staticが必要である。ちなみにジェネリックプログラミングやテンプレートメタプログラミングに登場するクラスは複数ポリシーのバインダや定数の器としての役割しかないヘルパクラスである場合が多い。つまり、テンプレートパラメータに使用できる型である点を除けば、namespaceの果たす機能と大差ない。<br/><br/>従って、主に実行時のオブジェクトの振る舞いや外部との関係をイメージするために意味を持つ、関数やデータメンバのカプセル化のための アクセス制限機構は、ここでは大した意味を持たない。テンプレートメタプログラミングで用いられるクラスはデータメンバもほとんど持たないのが一般的であり、それゆえに実行時の生成オーバーヘッドを気にすることなくこれらのクラス(小規模なクラスなのでstructが使用される場合が多い)をツールとして用いることが出来る。ただしconst変数を即値に展開しないコンパイラが存在する可能性を考慮する と、enumを使用する方が若干の気休めにはなるかもしれない。<br/><br/>さて定数で重要なのは、式の右辺はコンパイルの時点で計算されるということである。コンパイルの時点で決定しない物はenumの右辺に指定できないので至極当然ではある。<br/><br/><pre class="brush: cpp">struct C<br/>{<br/> enum {value = 1 + 1 == 2 ? 1 : 0};<br/>};</pre><br/><br/>定数式の中で三項演算子(? : )が用いられているのが奇異に映るかも知れない。しかし、条件として提示されている1 + 1 == 2という式はコンパイル時に判定でき、従って、valueには1が代入される。このことの含意は、条件式をコンパイル時に決定できる要素のみで組み立てれば、Cプリプロセッサマクロを用いなくてもコンパイル時に制御構文めいた構造が利用できるということである。<br/><br/>こうして、定数式の右辺に計算式を記入するだけで、コンパイラのための立派な計算プログラムが成立してしまう。例えば<br/><br/><pre class="brush: cpp">#include <iostream><br/><br/>struct C<br/>{<br/> enum {value = 24181 - 42917 * 242 % 2325 - 1245};<br/>};<br/><br/>int main()<br/>{<br/> std::cout << C::value << std::endl;<br/> return 0;<br/>}</pre><br/><br/>これで、コンパイラを走らせる度にではあるものの、実行時にCPU資源を全く消費することなく計算結果を得られる。現実的とはいえない例だが、テンプレートメタプログラミングとは結局のところこれ以上でも以下でもない。テンプレートすら未だに導入していないこの例の段階で断定するのは些か早計に見えても、テンプレートメタプログラミングの可能性と限界は実はここに顕著である。詳細は後に譲り、定数で構成された世界にもう一つの道具であるテンプレートを導入して状況がどう変化するか見てみよう。<br/><br/><pre class="brush: cpp">template <int x><br/>struct Square<br/>{<br/> enum {value = x * x};<br/>};</pre><br/><br/>テンプレートの導入によって、右辺に変数が導入され、方程式が表現できるようになった。ジェネリックプログラミングにおいてテンプレートクラスはparameterized typeと呼ばれ、その引数として型を取り、新しい型を生成するという、型オブジェクトを扱う関数のような存在である。この型は、classではなく整数のようなプリミティブ型をパラメータに指定した場合、具体的な数値となる。従ってパラメータがコンパイル時に決定されているならば<br/><br/><pre class="brush: cpp">#include <iostream><br/><br/>int main()<br/>{<br/> std::cout << Square<16>::value << std::endl;<br/> return 0;<br/>}</pre><br/><br/>のようにパラメータとして具体的な数値を与えて計算結果を得ることが出来る。これは、定数のみを使用していること、実行時に計算が行われていないことを除けば、<br/><br/><pre class="brush: cpp">int square(const int x)<br/>{<br/> return x * x;<br/>}<br/></pre><br/><br/>このような関数を実行しているのと結果は変わらない。記法については丸括弧が山括弧に変わっただけである。返り値 はクラスの定数(value)にアクセスして得る。この、テンプレートの山括弧を、関数呼び出しの丸括弧と同一視できるようになれば、テンプレートメタプログラミングに対する感覚的な素地は整ったと見て差し支えない。丸括弧で表記された関数ポインタ を介して関数が実行され制御が移ってゆくのが一般のC++プログラムコードを実行した場合の動的な評価である。これに対し、「テンプレートの展開」 がもたらすのは、コンパイラ上でのメタプログラムの評価という静的実行である。<br/><br/>次に「ループの展開」について見ていこう。ループをテンプレートで表現するには、動的実行のC++プログラムにおける、forによる繰り返し構文がアナロジーとして参考になる。for構文の丸括弧の中では、繰り返しに用いる条件の初期化/増分/終了条件を指定する。その繰り返しという営為の中の振る舞いを各々抽出すると、初期化/関数/終了という3つの段階が抽出できる。これはある法則を持つ数列の一部分を取り出して表す場合と同様である。ただし、前回の関数の実行結果をループの次のラウンドで使用するために、この関数の毎回の実行結果をコンテクスト情報として累積的に保存する変数(accumulator)が必要となる場合もある。<br/><br/>コンテクスト情報が保存されていれば、このループの各ラウンドを表す関数は定義時点でコンテクストを持たない単なる 関数定義ではなく、ループのコンテクストに拘束されコンテクスト情報と合成されたものとなる。これはいわゆる関数閉包 (closure)であり、たとえばRuby言語のイテレータ/ブロックも単なるループされる再入可能な関数ではなく同様の性格を持つ。 こうして得られたループの各要素をC++テンプレートを用いて表す場合、初期化は「テンプレートの部分的特殊化」という テクニックを用いて表し、ループされる関数閉包はテンプレートそのものを用いて表す。終了はすなわち最終的評価であるので、具体的なパラメータを与えてのコード上でのテンプレートのインスタンス化がそれに該当する。<br/><br/>ただし、単純にループさせる一区切りの関数としてのC++テンプレートを具体的な数値を与えてインスタンス化しただけでは、ループの最終ラウンドが実行されるだけである。そこで用いられるのが、再帰のテクニックだ。すなわち、インスタ ンス化するテンプレートの中に、前回ラウンドのテンプレートのインスタンスを含ませればよい。これにより、ループの最終ラウンド(n回目)のインスタンス化によってn-1回目のラウンドがインスタンス化/実行され、さらにn-2回目が...という調子で遡りながらループの各ラウンドがインスタンス化/実行されていく。ただし、ループの実行ラウンド数が負になることはありえないので、一番最初のラウンドまで戻ったときに遡及を停止しなければならない。これ は、先に初期化について述べたように、1回目のラウンドをテンプレートの部分的特殊化を用いて特殊化することによって解決される。<br/><br/>この再帰を用いたループ展開は、Lisp言語において、末尾再帰(tail-recursion)と呼ばれる再帰構造をループへ変換する作業の、ちょうど逆である。再帰よりはループの方がスタックを消費しない分パフォーマンスが上がりスタックオーバーフローの危険も無いというので手作業で末尾再帰構造をループに書き直すことがある。SchemeというLisp言語の方言では、処理系が末尾再帰を単なるジャンプへ変換するのでこの作業は要らず、全て再帰を用いて書いても問題ない。Lispでの末尾再帰のループ変換については、<a href="#R8">Lispテクニックの解説書</a>などを参照して頂きたい。<br/><br/>さてここでループに至る前に、より単純な、再帰やaccumulator無しで書ける単純な数列の表現と、テンプレートの部分的特殊化とを使う例として、実際に役に立ちそうなユーティリティクラスを作成してみることにする。複数の相互に排他的ではないフラグを表現するときに、ビットが立っているか否かで判定できるビットフラグを用いると空間効率の点で有利である。MS Windows環境では、たとえばWinGDI.hには<br/><br/><pre class="brush: cpp">/* PIXELFORMATDESCRIPTOR flags */<br/>#define PFD_DOUBLEBUFFER 0x00000001<br/>#define PFD_STEREO 0x00000002<br/>#define PFD_DRAW_TO_WINDOW 0x00000004<br/>#define PFD_DRAW_TO_BITMAP 0x00000008<br/>#define PFD_SUPPORT_GDI 0x00000010<br/>#define PFD_SUPPORT_OPENGL 0x00000020<br/>#define PFD_GENERIC_FORMAT 0x00000040<br/>#define PFD_NEED_PALETTE 0x00000080<br/>#define PFD_NEED_SYSTEM_PALETTE 0x00000100<br/>#define PFD_SWAP_EXCHANGE 0x00000200<br/>#define PFD_SWAP_COPY 0x00000400<br/>#define PFD_SWAP_LAYER_BUFFERS 0x00000800<br/>#define PFD_GENERIC_ACCELERATED 0x00001000<br/>#define PFD_SUPPORT_DIRECTDRAW 0x00002000<br/></pre><br/><br/>このようなフラグの定義が載っている。こうしたフラグに各ビットの立った数値を割り振ったものを、使うのはともかく自分で一々定義するのは煩わしいし、新しい要素を末尾ではなく中央に挿入したいときなど並べ直しも面倒である。各要素は立っているビットを順に1ビットずつ単に左シフトしたものにすぎない。そのような時に以下が使用できる。<br/><br/><pre class="brush: cpp">/* BitFlagVector.h */<br/><br/>#ifndef CALC_BITFLAGVECTOR_H<br/>#define CALC_BITFLAGVECTOR_H<br/><br/>// 先に部分的特殊化を行うためのテンプレートクラスの前方宣言<br/>// テンプレートクラス本体の定義の後で特殊化する場合は不要<br/><br/>template <int n> struct BitFlag;<br/><br/>// 具体的なパラメータ(0)によるテンプレートの部分的特殊化。<br/>// テンプレートパラメータが0の場合のみBitFlagについて<br/>// このテンプレートが適用される<br/>template <> struct BitFlag<0><br/>{<br/> enum {value = 0};<br/>};<br/><br/>// ビットフラグの値を表すテンプレート。<br/>// 1ビットを左シフトしていくことにより右から何番目(1-base)の<br/>// ビットが立っているかを表す<br/>template <int n><br/>struct BitFlag<br/>{<br/> enum {value = 1 << (n - 1)};<br/>};<br/><br/>// ビットフラッグのベクタを保持するテンプレート。<br/>// 保持のために使用する型(POD)をテンプレートパラメータとして取る<br/>template <typename T><br/>struct BitFlagVector<br/>{<br/> volatile T data_;<br/> BitFlagVector()<br/> : data_(0)<br/> {}<br/><br/> // i番目のビットを立てる<br/> template <int i><br/> void on()<br/> {<br/> data_ |= BitFlag<i>::value;<br/> }<br/><br/> // i番目のビットを下ろす<br/> template <int i><br/> void off()<br/> {<br/> data_ &= ~ BitFlag<i>::value;<br/> }<br/><br/> // i番目のビットが立っているかどうか<br/> template <int i><br/> bool is() const<br/> {<br/> return data_ & BitFlag<i>::value;<br/> }<br/><br/> // ベクタの初期化<br/> void reset()<br/> {<br/> data_ = 0;<br/> }<br/>};<br/><br/>// DWORD(32ビット)を保存できるベクタ<br/>typedef BitFlagVector<DWORD> BitFlagVector32;<br/><br/>#endif CALC_BITFLAGVECTOR_H<br/></pre><br/><br/>これを使用するときは、以下のようにenumと併せて使用する。<br/><br/><pre class="brush: cpp">class C<br/>{<br/>public:<br/> enum PFD<br/> { // 明示的な初期化のないenumは0から始まり1ずつ増える整数となる<br/> NONE, // = 0<br/> DOUBLEBUFFER,<br/> STEREO,<br/> DRAW_TO_WINDOW,<br/> DRAW_TO_BITMAP,<br/> SUPPORT_GDI,<br/> SUPPORT_OPENGL,<br/> GENERIC_FORMAT,<br/> NEED_PALETTE<br/> };<br/><br/> bool method()<br/> {<br/> flag_.on<PFD::STEREO>();<br/> flag_.off<PFD::SUPPORT_GDI>();<br/> return flag_.is<PFD::SUPPORT_OPENGL>();<br/> }<br/><br/>private:<br/> BitFlagVector32 flag_;<br/>};<br/></pre><br/><br/>これでenumの中にフラグの名称シンボルを列挙するだけで、対応するビットの立った値を手作業で割り付けることなくビットフラグが利用できるようになった。ただし、たとえば32ビットの領域をベクタの保持に選んだ場合、32個までしかビットフラグは利用できないので、enumに誤って32個より多くシンボルを入れた場合に正しくフラグが作れない。これが気になる場合は、<a href="#R4">Loki</a>のSTATIC_CHECKや<a href="#R5">Boost</a>の<a href="#R9">static_assert</a>を用いて、範囲外のフラグを使用していないかコンパイル時にコンパイラにチェックさせればよい。<br/><br/>次に、ループ展開について検討する。ループ展開は、コンパイラによる最適化の段階では、CPUサイクルを多く消費する可能性のある条件判定をできるだけ削除することによりオーバーヘッドを取り除くという作業である。テンプレートメタプログラミングでも同様に、各ラウンドの命令がループ回 数分並んでいるだけという状態の最終的なマシン語コードを生成することを目標として、テンプレートのインスタンス化の段階でループ展開を行う。<br/><br/>例として、符号理論で登場する、オーダーが256のガロアフィールド(GF256)での演算のための指数テーブルを、テンプレートを使ってコンパイル時に生成してみよう。テンプレートを用いずダイナミックに求める場合の計算式は以下のようになる。これを、コンパイル時に配列GEXPに指数表が格納されるようなテンプレートメタプログラムに変換することにする。<br/><br/><pre class="brush: cpp">int GEXP[512]; // 指数を格納するテーブル<br/>int p[9] = {0, 1, 0, 0, 0, 0, 0, 0, 0};<br/>GEXP[0] = 1;<br/>GEXP[255] = GEXP[0];<br/><br/>for (int i = 1; i < 256; ++i)<br/>{<br/> p[0] = p[8];<br/> p[8] = p[7];<br/> p[7] = p[6];<br/> p[6] = p[5];<br/> p[5] = p[4] ^ p[0];<br/> p[4] = p[3] ^ p[0];<br/> p[3] = p[2] ^ p[0];<br/> p[2] = p[1];<br/> p[1] = p[0];<br/> GEXP[i] = p[1] + p[2] * 2 + p[3] * 4 + p[4] * 8<br/> + p[5] * 16 + p[6] * 32 + p[7] * 64<br/> + p[8] * 128;<br/> GEXP[i + 255] = GEXP[i];<br/>}</pre><br/><br/>まず、先に検討したループ展開の各要素について見ていく。得るべき値はGEXPなのでこれを中心に、どのようにすればGEXPの各要素に望む値を入れることが出来るか考えていけばよい。まず初期化について、明らかにGEXP[0] = 1であり、しかもループがi == 1から始まっていることから逆に考えれば、i == 0のラウンドが特殊化されていると見ることが出来る。従って、まずはこれをテンプレートの部分的特殊化を用いて表現すればよい。つぎに、ループの各ラウンドの内容となる関数閉包について、見るからにごちゃごちゃしているのでどう分析したものか迷うかもしれない。ただ、ラウンドの最後の部分を見ると、最終的に必要な値はGEXPの各要素の値であり、そこに代入される値は、配列pの各要素を各項とする多項式の計算結果である。また、GEXP[256]からGEXP[510]は、各ラウンドのiの値をnとするとGEXP[n - 255]で表せる(ちなみにGEXP[511]は演算には不要なので0でよい)。<br/><br/>ここで多項式の各項pについて見ると、例えば最初のp[0] = p[8]ならば現在のラウンドのp[0]に一つ前のラウンドのp[8]を代入している。つまり、上で挙げたような再帰によってラウンドを遡及しながら最終ラウンドの値までを順に求めていく必要がある。ただし問題なのは、多項式の各項はそれぞれ他の項と個別に異なった関係を結んでいるという点である。そこで、多項式の各項pをそれぞれ別の容器に保持し、各項の関係をテンプレートで表現することによって、全体像を表現することにする。各項の配列pの初期化は、例によってテンプレートの部分的特殊化で表現できる。<br/><br/>以上の方針で変換したものが次のリストである。<br/><br/><pre class="brush: cpp"><br/>namespace GF256<br/>{<br/> // 多項式の各項をテンプレートで再帰的に表現する<br/> template <int i><br/> struct P<br/> {<br/> enum {<br/> p0 = P<i - 1>::p8,<br/> p1 = p0,<br/> p2 = P<i - 1>::p1,<br/> p3 = P<i - 1>::p2 ^ p0,<br/> p4 = P<i - 1>::p3 ^ p0,<br/> p5 = P<i - 1>::p4 ^ p0,<br/> p6 = P<i - 1>::p5,<br/> p7 = P<i - 1>::p6,<br/> p8 = P<i - 1>::p7,<br/> };<br/> };<br/><br/> // テンプレートの部分的特殊化による各項の初期化<br/> template <><br/> struct P<0><br/> {<br/> enum {<br/> p0 = 0,<br/> p1 = 1,<br/> p2 = 0,<br/> p3 = 0,<br/> p4 = 0,<br/> p5 = 0,<br/> p6 = 0,<br/> p7 = 0,<br/> p8 = 0<br/> };<br/> };<br/><br/> // 各指数のテンプレートによる表現。<br/> // コンパイラによっては再帰が複雑すぎるというコンパイラエラーが起きる<br/> // 場合があるのでi < 256の条件別に2つのテンプレートに分割してもよい<br/> template <int i><br/> struct E<br/> {<br/> enum {value = i < 256 ?<br/> (P<i>::p1 + P<i>::p2 * 2 + P<i>::p3 * 4<br/> + P<i>::p4 * 8 + P<i>::p5 * 16<br/> + P<i>::p6 * 32 + P<i>::p7 * 64<br/> + P<i>::p8 * 128)<br/> : E<i - 255>::value<br/> };<br/> };<br/><br/> template <><br/> struct E<0><br/> {<br/> enum {value = 1};<br/> };<br/><br/> // GEXP[255] = GEXP[0]の特殊化による表現<br/> template <><br/> struct E<255><br/> {<br/> enum {value = E<0>::value};<br/> };<br/><br/> // 各指数をインスタンス化し、定数のテーブルを初期化する<br/> const int GEXP[512] = {<br/> E<0>::value, E<1>::value, E<2>::value, E<3>::value,<br/> E<4>::value, E<5>::value, E<6>::value, E<7>::value,<br/> /* 中略*/<br/> E<504>::value, E<505>::value, E<506>::value,<br/> E<507>::value, E<508>::value, E<509>::value,<br/> E<510>::value,<br/> 0<br/> };<br/>};</pre><br/><br/>これによって、プログラムの実行の最初に初期化関数を実行して得られたデータを以後シングルトンとして管理する等の作業から解放される。<br/><br/>ただし問題点もある。一見して気になるのが、最下部のGEXPの表である。ここには、511(+1)個分のEのインスタンスが入る。面倒な作業なのでPerlなどのスクリプトで生成してソースコードにペーストするしかない。 プログラム内で表の中のどの数を使用するかがあらかじめ決定できるわけではないので、静的な世界と動的な世界の橋渡しとして、このような表が必要になるのは致し方ない。<br/><br/>また、もっと深刻な問題は、このプログラムのコンパイルにかかる時間である。実はこれが一番最初に書いたテンプレートメタプログラミン グの限界を画する重要な問題に他ならない。コンパイラのテンプレートに対する最適化具合にもよるが、上記のようなテンプレートを展開しコンパイルするには相当なリソースが必要となって しまうのである。そこで、残念ながら、私ならば上記のプログラムを使わずに、昔ながらの定数テーブルをプログラムの中にペーストする方を選んでしまうだろう。付け加えると、本来的には、テンプレートメタプログラムのインスタンスは定数テーブルと同一の機械語コードを生成しなければならないはずだが、実際にそうなるかはコンパイラ次第なので、テンプレートメタプログラムの作成、ビルド、実行の各段階にかかるコストの見積に失敗すると痛い目に遭う。このトピッ クについては<a href="#R10">David Abrahamsらによる考察</a>に詳しいのでそちらを参照して頂きたい。<br/><br/>最後に、同じGF256の、逆対数を求めるテーブルをテンプレートメタプログラムに変換し、静的な制御構造について学習する。動的に表を作るには、以下のプログラムを実行する。<br/><br/><pre class="brush: cpp"><br/>int GAL[256]; // 逆対数を保持するテーブル<br/><br/>GAL[0] = 1;<br/>int p = 1;<br/>for (int i = 1; i < 256; i++)<br/>{<br/> if (((p << 1) < (1 << 9))<br/> && ((p << 1) & (1 << 8)))<br/> GAL[i] = (p << 1)<br/> ^ ((1 << 8) | (1 << 4)<br/> | (1 << 3) | (1 << 2) | 1);<br/> else<br/> GAL[i] = p << 1;<br/>}<br/></pre><br/><br/>一見して指数表の場合と異なるのは、if...elseブロックによる条件分岐がある点ぐらいか。単純にGAL[i] に値を代入しているだけなので、上で三項演算子(?:)を使用したのと同様にvalueに値を入れる構造を作れば十分である。ここでは、別の方法によるテンプレートメタプログラム化を試してみることにする。<br/><br/><pre class="brush: cpp"><br/>template<<br/> bool Cond,<br/> typename TrueReturn,<br/> typename FalseReturn<br/>><br/>struct StaticIf<br/>{<br/> template<bool C><br/> struct Return<br/> {<br/> typedef TrueReturn R;<br/> };<br/><br/> template<><br/> struct Return<false><br/> {<br/> typedef FalseReturn R;<br/> };<br/><br/> typedef typename Return<Cond>::R result_type;<br/>};<br/><br/>namespace GF256<br/>{<br/> template <int i><br/> struct T<br/> {<br/> enum {<br/> value = (i << 1) ^ ((1 << 8)<br/> | (1 << 4) | (1 << 3)<br/> | (1 << 2) | 1)<br/> };<br/> };<br/><br/> template <int i><br/> struct F<br/> {<br/> enum {value = i << 1};<br/> };<br/><br/> template <int i><br/> struct AL<br/> {<br/> enum {value = StaticIf<<br/> ((AL<i - 1>::value << 1)<br/> < (1 << 9))<br/> && ((AL<i - 1>::value << 1)<br/> & (1 << 8)),<br/> T<AL<i - 1>::value - 1>,<br/> F<AL<i - 1>::value - 1><br/> >::result_type::value};<br/> };<br/><br/> template <><br/> struct AL<0><br/> {<br/> enum {value = 1};<br/> };<br/><br/> const int GAL[256] = {<br/> AL<0>::value, AL<1>::value, AL<2>::value,<br/> /* 中略 */<br/> AL<253>::value, AL<254>::value, AL<255>::value<br/> };<br/>};<br/></pre><br/><br/>StaticIfクラステンプレートが、コンパイル時に評価されるifの表現である(ちなみに、<a href="#R5">Boost</a>の<a href="#R6">MPL</a>に ほとんど同じものが一式揃っている)。テンプレートパラメータbool Condが真か偽かにより、Returnクラステンプレートの元のテンプレートか部分的特殊化バージョンのどちらかがインスタンス化され、それによってな し崩し的にtype_nameがテンプレートパラメータTrueReturnかFalseReturnのどちらかを表すようにtypedefされる。ここではtypedefによって型そのものがあたかも値であるかのように保持され、操作されている。言い換えれば、typedefされたエイリアス型は、型を保持するスロットであり、上記で使用してきた enumが定数に対するそれであるのと同様にはたらく。そのようにして結果的に得られた型については、これもenumの場合と同様に、 StaticIf<...>::result_typeのようにスコープ演算子を用いてアクセスする。<br/><br/>テンプレート内でのtypedefの使用や型の操作については、例えばSTLアロケータでのrebindは、型とテンプレート型を伝播させていくのに便利な手段として使用される(詳しくは<a href="#R11">Robert Schmidtによるコラム</a>を参照されたい)。また、<a href="#R4">Loki</a>に おけるtypelistは、非常に強力な型操作のためのツールである(ただし、テンプレートメタプログラミングというよりはジェネリックプログラミングの範疇に属する)。typelistは、今のところマクロで実現されているとはいえ、極めて利用価値が高く、将来のC++標準へ言語機能として組み込まれることが望ましい機能の1つである。<br/><br/>さらに、例に登場させてはいないものの、sizeof演算子は型と定数という2つの要素を関連付け組み合わせるのに有用なツールである。sizeofは静的に適用でき、その結果はenumなどの定数に保持できる。つまり、テンプレートメタプログラミングの文脈では、型を定数に変換する ツールであると考えればよい。ただし、型によってサイズが異なるとは限らないので、サイズと型を一対一に対応させたり相互変換ができたりするわけではな く、限定的な利用形態に留まる。<br/><br/>以上、テンプレートメタプログラミングについて基本を一通り見てきた。単なるシンタックスシュガーを超えてC++テンプレートには非常に大きな可能性があること、現在のC++の最重要機能はテンプレートであることを意識して頂ければ幸いである。<br/><br/><strong>参考文献:</strong><br/><br/><a id="R1" name="R1"></a>[1] <a href="http://www.oonumerics.org/blitz/" target="_blank">The Blitz++ Library</a><br/><br/><a id="R2" name="R2"></a>[2] Todd Veldhuizen, <a href="http://osl.iu.edu/%7Etveldhui/papers/Template-Metaprograms/" target="_blank"><em>Template Metaprograms</em></a><br/><br/><a id="R3" name="R3"></a>[3] Andrei Alexandrescu, <em><a href="http://www.amazon.co.jp/gp/product/0201704315?ie=UTF8&tag=klassphere-22&linkCode=as2&camp=247&creative=1211&creativeASIN=0201704315" target="_blank">Modern C++ Design: Generic Programming and Design Patterns Applied (C++ in Depth Series)</a><img src="http://www.assoc-amazon.jp/e/ir?t=klassphere-22&l=as2&o=9&a=0201704315" border="0" alt="" width="1" height="1" /></em>, Addison Wesley 2001 (<a href="http://www.amazon.co.jp/gp/product/4894714353?ie=UTF8&tag=klassphere-22&linkCode=as2&camp=247&creative=1211&creativeASIN=4894714353" target="_blank">邦訳</a><img src="http://www.assoc-amazon.jp/e/ir?t=klassphere-22&l=as2&o=9&a=4894714353" border="0" alt="" width="1" height="1" />)<br/><br/><a id="R4" name="R4"></a>[4] <a href="http://sourceforge.net/projects/loki-lib/" target="_blank">Loki: A C++ library of designs, containing flexible implementations of common design patterns and idioms</a><br/><br/><a id="R5" name="R5"></a>[5] <a href="http://www.boost.org/" target="_blank">The Boost C++ Library</a><br/><br/><a id="R6" name="R6"></a>[6] <a href="http://www.boost.org/libs/mpl/doc/index.html" target="_blank">The Boost MPL Library</a><br/><br/><a id="R7" name="R7"></a>[7] James O. Coplien, <em><a href="http://www.amazon.co.jp/gp/product/0201824671?ie=UTF8&tag=klassphere-22&linkCode=as2&camp=247&creative=1211&creativeASIN=0201824671" target="_blank">Multi-Paradigm Design for C++</a><img src="http://www.assoc-amazon.jp/e/ir?t=klassphere-22&l=as2&o=9&a=0201824671" border="0" alt="" width="1" height="1" /></em>, Addison Wesley 1999 (<a href="http://www.amazon.co.jp/gp/product/4894712989?ie=UTF8&tag=klassphere-22&linkCode=as2&camp=247&creative=1211&creativeASIN=4894712989" target="_blank">邦訳</a><img src="http://www.assoc-amazon.jp/e/ir?t=klassphere-22&l=as2&o=9&a=4894712989" border="0" alt="" width="1" height="1" />)<br/><br/><a id="R8" name="R8"></a>[8] Paul Graham, <em><a href="http://www.paulgraham.com/onlisp.html" target="_blank">On Lisp</a></em>, Prentice Hall, 1993 (webで全文閲覧可能)<br/><br/><a id="R9" name="R9"></a>[9] <a href="http://www.boost.org/libs/static_assert/static_assert.htm" target="_blank">static_assert</a> in <a href="http://www.boost.org/libs/mpl/doc/index.html" target="_blank">The Boost C++ Library</a><br/><br/><a id="R10" name="R10"></a>[10] David Abrahams and Carlos Pinto Coelho, <em><a href="http://users.rcn.com/abrahams/instantiation_speed/" target="_blank">Effects of Metaprogramming Style on Compilation Time</a></em><br/><br/><a id="R11" name="R11"></a>[11] Robert Schmidt, <em><a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dndeepc/html/deep08032000.asp" target="_blank">Deep C++: Typedef Templates</a></em>, Microsoft Corporation Aug. 2000久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0tag:blogger.com,1999:blog-1046418983081373167.post-56517952797400223552003-07-25T16:45:00.000+09:002015-02-26T02:49:09.263+09:00IN SOVIET RUSSIA DICE VERSION 0.8 RELEASES YOU!やれやれやっとDICE 0.8の配布にこぎ着けることが出来て感慨深い。というくらいリリースが難航して結果約2か月も予定より遅れることになってしまった。その代わりと言っては何だが新しくWebサーバ機能も入り、かつ安定性も大幅に向上している(特にWindows XP/Windows Server 2003上)。「IRC/HTTP/opennapハイブリッドサーバ for MS Windows 2000/XP/2003」なんていかにも危なっかしい形容なのでIISのように簡潔な名称にしたいところではある。そこで私自身忘れていたのは、実はDICEという名前がその役目を果たすべきものであるにもかかわらず、未だに何故DICEという名前なのかどこにも由来を書いていないということである。今回も書かないのだけれども代わりにDICEに関する今後の方向性を示唆する物として<a href="http://www.irc101.org/interviews.php?id=352" target="_blank">IFIRCでのインタビュー</a>を参照されたい(これも微妙に古くなってしまったが)。<br/><a name='more'></a><br/>下でゲームやらないと書いた割には最近またやり始めている。とはいってもやりこんだのは<a href="http://www.rockstargames.com/vicecity/" target="_blank">GTA Vice City</a>と<a href="http://www.chasergame.com/" target="_blank">Chaser</a>だけだ。<br/><br/><a href="http://www.rockstargames.com/vicecity/" target="_blank">GTA Vice City</a>はまずサイトのflashがとても洗練されていて印象が良くまずはそこから入った。誰もが褒めちぎるタイトルなので一々付け加えるのも無粋だが、前作 GTA3があまり気に入らなかった人間から言わせてもらえば、前作からそれほど時間を置いていないにもかかわらず大幅にクオリティが向上している。前作では残虐性の軽減を図ったのか人形のようだった通行人達もそれほど軽々しくなくむしろGTAアートワークのアニメ/トゥーンシェーダっぽい印象をそのまま取 り込んだようなバランスの良さである。バイクでの疾走も他のバイクレースゲームのように決して操作が困難ではなく、またMafiaのように車中でダメージ を受けることもないので安心してドライブを楽しめる。そして音楽が80年代ヒットの数々なのだ。ゲーム中の車で目的地へ飛ばしつつBilly JeanがFlash FMでかかったときの爽快感は、リアルな楽曲をゲーム中で使用するというアイデアへ半信半疑だった私の不安を遙かに裏切って新鮮だった。なにせリアルでイイ ものはゲーム内でも文句なくイイのである。残虐性で名高いこのシリーズにBilly Jeanの作者がライセンスを下したという事実もまた興味深いものの、このゲーム全体を貫くノリのよさ、快感原則への忠実性はほとんど禁じ手のようですらある。<br/><br/><a href="http://www.chasergame.com/" target="_blank">Chaser</a>は、 それに対し、最初期待していなかった時点での予想水準は上回ったものの、しかしクリティカルヒットでは決してないという微妙な作品だった。よくある近未来物で、かつビジュアル面とプレイ時間の長さでは一級である。また、FPSの中でこれほど開けた空間の拡がりを強調したものは初めてだったのでその点はまず手放しで誉めたい。しかし、ストーリーやゲームバランスなど細部で微妙に違和感が残った。たとえば、似たような近未来物でもDeus Exのように作者らがこだわりを持って作り込めば非常に濃い作品となり、そのこだわり(Deus Exで言えばアメリカの都市伝説としての陰謀論や異星人などいわゆるトンデモ系)に全く共感を持てない私のような人間にも、プロデューサーが強引に押し付ける鬼気迫る強迫観念を共有できるような気にさせてくれるところがあるのである。それに対し、<a href="http://www.chasergame.com/" target="_blank">Chaser</a>の場合中途半端に料理された感が否めない。皆がてんでばらばらに好き勝手にやった感じがあり、中盤のしっかりした作りに比べると前半と終盤は全然別のチームが作ったのではないかというのが私の推測である。どうも<a href="http://www.jowood.com/" target="_blank">JoWood</a>というパブリッシャはバランスの悪い作品ばかり取り揃えている気がしてしまう。実際の私のゲームプレイ体験はと言えばちょうど<a href="http://www.devastationgame.com/" target="_blank">Devastation</a>の後遺症が発現したのか、FPSであるこのゲームをやりながらゲロを吐きそうになった。それは閉所恐怖症の人間を追いつめるシーンが多いこのゲームのレベル 設計だけの問題だけではあるまい。ところでそんな生焼けのゲームでも笑える点には事欠かない。主人公が宇宙から降りてくるときにいつのまにか革ジャンとジーンズを着ていたり、水中をアクアラングを付けて長期潜行するときも同じ服装だったり、日本人女科学者が会って15秒で殺害されたり、リトル・トウキョ ウのヤクザの用心棒が特撮ヒーローのような格好をしていたりする(実は非常に弱い)のである。<br/><br/>既に半分が過ぎてしまった2003年だが、このblogの2002年12月21日の項での予想を見てみると、Unreal IIがリリース済である他は未リリースである。ただしDungeon Siegeは拡張パックが近々出るようで、またDeus Ex 2もかなり完成に近づいているはずだ。Doom 3が延期になってしまったので、あとはHalo PCやHalf-Life 2くらいしか楽しみにできる作品がない。この状況を見ていると、WindowsにおけるPCゲームも量産されるRTSやオンラインゲームを除けば、いよいよ衰退が顕在化してきたようだ。久富木 隆一 (KUBUKI Ryuichi)http://www.blogger.com/profile/01140740424497077021noreply@blogger.com0