2012年5月29日 星期二

[LLVM] 編譯、使用 Clang Plugin @ Ubuntu 10.04 64Bit


圖: http://llvm.org/Logo.html


身為速食工作者,我本身不太喜歡浪費太多時間在瑣碎的細節,在加上本業非 Compiler 背景,只是從 Google 那邊翻到一些編譯、執行 Clang Plugin 的方式,順便筆記下來,畢竟網路上的資料真的不多,也該為繁體中文留下點足跡吧。


操作環境:


OS: Ubuntu Server 64Bit
llvm src location: ~/llvm
clang src location: ~/llvm/tools/clang
gcc: gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3


首先依照 LLVM 官網介紹的方式,下載 LLVM、clang 等相關程式碼,接著編譯,之後換到 llvm/tools/clang/example 目錄中,裡頭有一個 clang plugin 範例程式 PrintFunctionNames,僅需切換進入此目錄打 make 後,即可編出來 (細節都寫在llvm/tools/clang/examples/PrintFunctionNames/README.txt)


其中,有兩種編譯方式,第一種是直接在 llvm 目錄下,執行 configure 後再打 make -j4 即可編譯出 llvm 和 clang,接著切換到 llvm/tools/clang/examples/PrintFunctionNames 裡頭在執行 make 後,東西就產生了;另一個編法是用 cmake 把 source 跟產出分開,例如 mkdir ~/build && cd ~/build && cmake ~/llvm && make -j4 後,接著切換到 ~/build/tools/clang/examples/PrintFunctionNames,執行 make 後,產出物在 ~/build/lib/PrintFunctionNames.so。由於使用官方推薦的 configure 方式有碰到問題,所以我就改用 cmake 方式,也意外發現 cmake 編得比較快(不知有沒遺漏什麽)。


編譯完,就可以用 clang -ccl -load /path/libPrintFunctionNames.so -plugin print-fns some-input-file.c 執行。(若用 cmake 編出的是 /path/PrintFunctionNames.so)


大部分的 clang plugin 範例就是從 PrintFunctionNames 改來的,介紹一下 PrintFunctionNames 的使用方式:


$ vim t1.c
int main() {
return 0;
}

$ clang -ccl -load /path/PrintFunctionNames.so -plugin print-fns t1.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
top-level-decl: "__builtin_va_list"
top-level-decl: "main"

$ vim t2.c
#include
int main(int argc,char *argv[] ) {
printf("Hello World!\n");
return 0;
}

$ clang -ccl -load /path/PrintFunctionNames.so -plugin print-fns t2.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
/path/t2.c:1:10: fatal error: 'stdio.h' file not found
#include
^
top-level-decl: "__builtin_va_list"
top-level-decl: "main"
1 error generated.

這是因為 clang 不知哪邊找 stdio.h 檔,所以就透過 -I 指令去指定吧(過程中還需 stddef.h)


$ clang -cc1 -I/usr/include -I/usr/lib/gcc/x86_64-linux-gnu/4.4.3/include -load /path/PrintFunctionNames.so -plugin print-fns ~/t2.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
top-level-decl: "__builtin_va_list"
top-level-decl: "size_t"
...
top-level-decl: "ctermid"
top-level-decl: "flockfile"
top-level-decl: "ftrylockfile"
top-level-decl: "funlockfile"
top-level-decl: "main"

其中 -I/usr/include 是找 stdio.h,而 I/usr/lib/gcc/x86_64-linux-gnu/4.4.3/include 是找 stddef.h (依個人安裝位置不同)


然而,每次執行都用 clang -load *.so -plugin ... 指令進行有點麻煩,所以另一種使用方式就是寫隻 main 程式,在裡頭呼叫 clang 相關函數物件來操作,這樣的好處是可以編出一隻 tool 來用,也不用每次執行時下很長的指令了:


$ mkdir ~/print-fns-tools
$ cp ~/llvm/tools/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp ~/print-fns-tools/main.cpp
$ vim ~/print-fns-tools/main.cpp

// 在檔案最後面新增程式碼
#include "llvm/Support/Host.h"
#include "clang/Parse/ParseAST.h"
#include "clang/Basic/FileManager.h"


#include "clang/Frontend/HeaderSearchOptions.h"
#include "clang/Frontend/Utils.h"


#include <iostream>
int main(int argc, char *argv[]) {
        clang::CompilerInstance *ci = new clang::CompilerInstance();
        ci->createDiagnostics(0,NULL);
        clang::TargetOptions to;
        to.Triple = llvm::sys::getDefaultTargetTriple();
        clang::TargetInfo *pti = TargetInfo::CreateTargetInfo(ci->getDiagnostics(), to);
        ci->setTarget(pti);
        ci->createFileManager();
        ci->createSourceManager( ci->getFileManager() );
        ci->createPreprocessor();
        ci->createASTContext();


        if( argc < 2 ) {
                std::cout << "Usage: " << argv[0] << " [-I /usr/include] file.c " << std::endl;
                return 0;
        } else if ( argc >= 4 ) {
                clang::HeaderSearchOptions headerSearchOptions;
                for( int i=1 ; i<argc-1 ; ++i )
                        if( argv[i][0] == '-' && argv[i][1] == 'I' && i+1 < argc ) {
                                std::cout << "Search header: " << argv[i+1] << std::endl;
                                headerSearchOptions.AddPath( argv[++i] , clang::frontend::Angled, false, false, false );
                        }


                clang::PreprocessorOptions preprocessorOptions;
                clang::FrontendOptions frontendOptions;
                clang::InitializePreprocessor( ci->getPreprocessor(), preprocessorOptions, headerSearchOptions, frontendOptions );
        }


        std::cout << "Target: " << argv[argc-1] << std::endl;
        ci->getSourceManager().createMainFileID( ci->getFileManager().getFile(argv[argc-1]) );


        PrintFunctionsConsumer *mConsumer = new PrintFunctionsConsumer();
        ParseAST(ci->getPreprocessor(), mConsumer, ci->getASTContext());


        delete ci;
        delete mConsumer;


        return 0;
}


直接編譯 main.cpp (假設環境變數中可以取得 clang++、llvm-config 指令,否則取代成 /path/clang++、/path/llvm-config 的用法):


$ cd print-fns-tools/
$ clang++ `llvm-config --cxxflags` -fno-rtti main.cpp -lclangFrontend -lclangDriver -lclangSerialization -lclangParse -lclangSema -lclangAnalysis -lclangRewrite -lclangEdit -lclangAST -lclangLex -lclangBasic -lLLVMMC -lLLVMSupport `llvm-config --ldflags --libs cppbackend`


操作使用:


$ ./a.out
Usage: ./a.out [-I /usr/include] file.c

$ ./a.out t1.c
Target: t1.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
top-level-decl: "__builtin_va_list"
top-level-decl: "main"

$ ./a.out t2.c
Target: t2.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
a.out: /path/llvm/tools/clang/lib/Frontend/TextDiagnosticPrinter.cpp:158: virtual void clang::TextDiagnosticPrinter::HandleDiagnostic(clang::DiagnosticsEngine::Level, const clang::Diagnostic&): Assertion `TextDiag && "Unexpected diagnostic outside source file processing"' failed.
Stack dump:
0. t2.c:1:2: current parser token 'include'
Aborted

$ ./a.out -I /usr/include -I /usr/lib/gcc/x86_64-linux-gnu/4.4.3/include t2.c
Search header: /usr/include
Search header: /usr/lib/gcc/x86_64-linux-gnu/4.4.3/include
Target: t2.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
top-level-decl: "__builtin_va_list"
top-level-decl: "size_t"
...
top-level-decl: "ftrylockfile"
top-level-decl: "funlockfile"
top-level-decl: "main"

總結一下,原先使用 clang -plugin 的方式啟動,本身有 clang 這個環境可以操作,而單純寫成一隻 toolbase 方式,則需要初始化 clang 的操作環境,這就是在 main 裡頭做的事,包含建立一個 Compiler Instance 及其初始化、設定 header search 位置、要處理的 file.c 等等,如此一來就完成了。


其他筆記(用 clang++ 編譯時出現訊息): 



  • undefined reference to `typeinfo for clang::ASTConsumer'


    • 編譯時加上 "-fno-rtti" 參數即可解決


  • error: no member named 'getDefaultTargetTriple' in namespace 'llvm::sys'
    to.Triple = llvm::sys::getDefaultTargetTriple();


    • 加上 #include "llvm/Support/Host.h" 即可解決


  • error: member access into incomplete type 'clang::FileManager'
    note: forward declaration of 'clang::FileManager'


    • 加上 #include "clang/Basic/FileManager.h" 即可解決 



最後一提,網路上資源不見得少,但因為語系與背景不同,常常關鍵字下很久都找不到,無意間還找到 Google 員工在去年論壇討論串裡,正抱怨著 clang plugin 很難寫 XDD 當下我才發現,大家都是普通人 XDDD 此外,有興趣可以多多參考這裡:https://github.com/loarabia/Clang-tutorial,裡頭有簡單又豐富的程式碼、makefile 喔。


2012年5月28日 星期一

[iOS] 使用 absinthe-win-2.0.2 來 Jailbreak iPad 1 iOS 5.1.1 @ VirtualBox


圖:http://greenpois0n.com/


這年頭 Jailbreak 越來越方便 XD 最重要的是漸漸可以用 VirtualBox 完工了!甚至 JB 程式不只 WindowsMac 版,還推出了 Linux 版。真是有夠威。建議要使用的人還是連去官網下載吧(還用Google Docs哩)!以免不小心抓到木馬。


jailbreak jailbreak_done


此次環境:


Guest OS:Windows XP SP3
Host OS:Windows 7 64Bit
Target:iPad 1 WiFi 版 with iOS 5.1.1 (9B206)


首先把 iPad 1 升到 iOS 5.1.1,再來使用 VirtualBox 來進行 JB,在 C:\ 點選剛下載好的 absinthe-win-2.0.2.exe 後自動解壓縮一個目錄,接著再進去點選該執行檔即可。(如果 iPad 1 已在 JB 狀態下了,無法透過網路升級時,可直接下載 iPad1,1_5.1.1_9B206_Restore.ipsw 並透過 iTunes 回復方式來升級)


如果使用上執行進度卡住時,可以試試把 VirtualBox 重新接上 iPad 看看,畢竟 iPad 裝置有 3 個 status,每一個狀態被偵測的項目是不一樣的,如 Apple Inc. Apple Mobile Device (Recovery Mode)、Apple Inc. Apple Mobile Device (DFU Mode)、Apple Inc. iPad [0001] 等等或是把 iPad 各個狀態都在 VirtualBox 設定好 USB 自動偵測使用應該也能解掉,此外也要留意 Host 上頭若有裝 iTunes 程式,要小心這些程式佔用資源,將導致 VirtualBox 無法取用 iPad 喔,解法大概就用 taskmgr 去把 AppleMobileDeviceService.exe 、iPosService.exe 等相關的關掉在試看看吧


2012年5月26日 星期六

[LLVM] LLVM、Clang、Clang Plugin、Clang Rewriter 筆記 @ Ubuntu 64bit

Source: http://en.wikipedia.org/wiki/LLVM
圖片來源:http://llvm.org/Logo.html


第一次看到 Open source project 覺得 LOGO 很殺 XD 這也代表 LLVM 真的有某個層度的威力!LLVM 全名為 Low Level Virtual Machine,翻譯為底層虛擬機器,並且從 wiki 介紹可以略知一二,就是透過虛擬技術來作最佳化。



  • http://llvm.org/

  • http://en.wikipedia.org/wiki/LLVM


    • LLVM (formerly Low Level Virtual Machine) is a compiler infrastructure written in C++ that is designed for compile-time, link-time, run-time, and "idle-time" optimization of programs written in arbitrary programming languages.


  • http://zh.wikipedia.org/zh-tw/LLVM


    • LLVM ,命名最早源自於底層虛擬機器(Low Level Virtual Machine)的縮寫[1]。它是一個編譯器的基礎建設,以C++寫成。它是為了任意一種程式語言寫成的程式,利用虛擬技術,創造出編譯時期,鏈結時期,執行時期以及「閒置時期」的最佳化。



談論到虛擬機器,對資訊領域最親近的程式語言是 Java,以及 Java bytecode、Java Virtual Machine (JVM) 和跨平台的關係。將 Java 程式碼 (.java) 編譯成 bytecode (.class) 後,透過 JVM 可執行結果,加上多種平台都有 JVM (Windows/Linux/Mac),提供開發者只需撰寫一份程式碼、僅需編譯一次,接著就可以拿著編譯完的結果到各類有支援 JVM 的平台執行,成果就達成了跨平台。若粗淺地看 LLVM 的話,也有類似的關係,只是這時架構稍微調整成:


N 種程式語言 (front ends) > 產生 LLVM Bitcode > 產生 M 個程式執行環境 (back ends)


而 LLVM 能做的事不止於此,還可進行程式效能最佳化處理等等。


首先提一下安裝 LLVM 的部分好了,參考官方文件的介紹,在此選擇 Ubuntu 64bit 環境:


$ uname -a
Linux 2.6.32-38-server #83-Ubuntu SMP Wed Jan 4 11:26:59 UTC 2012 x86_64 GNU/Linux
$ dmesg | grep -i cpu
Intel(R) Xeon(R) CPU E5420 @ 2.50GHz

下載程式碼:


$ cd ~/ && svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
$ cd ~/llvm/tools/ && svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
$ cd ~/llvm/projects && svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt
$ cd ~/llvm/projects && svn co http://llvm.org/svn/llvm-project/test-suite/trunk test-suite

採用官方編譯方式:


$ cd ~/ && mkdir -p build out && cd ~/build && ../llvm/configure --prefix=$HOME/out
$ time make -j8
real 9m55.952s
user 47m7.270s
sys 5m49.610s

採用 cmake 編譯方式:


$ sudo apt-get install cmake
$ cd ~/ && mkdir -p build out && cd ~/build && cmake -DCMAKE_INSTALL_PREFIX:PATH=$HOME/out ../llvm
$ time make -j8
real 5m39.994s
user 37m23.290s
sys 4m27.790s

原先是某次用 configure 編譯會過不了(trunk版本常碰到的現象),就嘗試用 CMAKE ,結果一用發現編得更快,從此我就都用 CMAKE 了 XDDD (或許有少編啥東西 :P 但不影響我研究,就沒計較了) 須留意編譯 Clang plugin 的部分,如果 llvm 採用 configure 編法,那只需切換到 clang plugin project 中執行 make 即可,其他細節可參考 PrintFunctionNames 裡的 README,如果採用 CMAKE 編法,記得要跑去 CMake 產生得相對目錄結構中,執行 make 才會編出來,並且編出來的位置是擺在在 lib 中(此例是~/build/lib)。


研究一下 clang 的用處好了,最直觀的用途是像 gcc 的編譯器功能(並非全部功能都支援, 這跟LLVM架構有關),把 source code 編譯成執行檔,而 compiler 有很多種,例如 one-pass 跟 multi-pass compiler,我想 LLVM 架構應該算 multi-pass 的,由 clang 作為 front end 的部份,將程式碼編譯成 LLVM Bitcode,接著在經由 LLVM 架構,搭配 back ends 端產出各種平台的 native code。而 clang 的功能不只於此,參考 Google 發表的相關文章 C++ at Google: Here Be Dragons,可以透過 clang 來處理一些錯誤偵錯,例如 bool *charge_acct 變數,操作上不小心弄成 charge_acct = false 的問題(正確使用應該是 charge_acct = NULL 或 *charge_acct = false),更多介紹請參考 Chromium Style Checker Errors。簡言之,透過 clang 可以幫忙掃 source code 找一些容易忽略的錯誤,並且輸出人性化的訊息。相關產品: Clang Static Analyzer


由於 Clang 可以分析 source code,那就存在很多玩法,且架構上提供 clang plugin 用法。範例程式:clang/examples/PrintFunctionNames/PrintFunctionNames.cpp,翻閱一下,可以看到一些關鍵字:PluginASTActionASTConsumer,其中 AST 在 Compiler 界全名為 Abstract Syntax Tree,就是把程式碼建立一個 tree 結構,方便分析。


至於寫一個 Clang Plugin 要做的事:(可從範例程式 PrintFunctionNames 複製一份再來修改)



  1. 撰寫 PluginASTAction


    • 關於 PluginASTAction 架構可參考 http://clang.llvm.org/doxygen/classclang_1_1PluginASTAction.html, 簡言之,包括 CreateASTConsumer 與 ParseArgs,其中 ParseArgs 可透過參數來初始化 plugin 操作環境或是區分要執行那個動作,而 CreateASTConsumer 則是核心項目,也就是依工作目的建立自己的 ASTConsumer。


  2. 撰寫 ASTConsumer


  3. 註冊操作時使用的名稱


    • 此 PrintFunctionNames 為 print-fns


  4. 編譯產生 xxx.so 或在 Mac 為 xxx.dylib 檔


    • 此 PrintFunctionNames  為 PrintFunctionNames.so 或 libPrintFunctionNames.so


  5. 操作使用 clang -cc1 -load /path/xxx.so -plugin YourPluginName YourInputFile 或 clang -cc1 -load /path/xxx.so -plugin YourPluginName -plugin-arg-YourPluginName YourArgs YourInputFile


    • clang -cc1 -load /path/PrintFunctionNames.so -plugin print-fns some-input-file.c

    • clang -cc1 -load /path/PrintFunctionNames.so -plugin print-fns -plugin-arg-print-fns -an-error some-input-file.c



在 PrintFunctionNames 例子來說,在 PluginASTAction::ParseArgs 有一個簡易判斷參數的架構;在 ASTConsumer 裡頭,使用 HandleTopLevelDecl 功能,用來輸出程式碼內的 top level function name。因此,若要寫 Clang Plugin 的話,可以從 PrintFunctionNames 的架構修改。接著更複雜的應用就是 source-to-source translation,可用 Google 搜尋 clang source to source translation 或 clang rewriter 看看,最經典的例子是 lib/Rewrite/RewriteObjC.cpp


使用 clang rewriter 進行 source to source translation,將 ObjC 程式碼轉成 C 語言:


$ sudo apt-get install llvm clang g++
$ vim Hello.m
#import
int main( int argc, const char *argv[] ) {
printf( "hello world\n" );
return 0;
}
$ clang -rewrite-objc Hello.m
$ cat Hello.cpp
#ifndef __OBJC2__
#define __OBJC2__
#endif
struct objc_selector; struct objc_class;
struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};
#ifndef _REWRITER_typedef_Protocol
typedef struct objc_object Protocol;
#define _REWRITER_typedef_Protocol
#endif
#define __OBJC_RW_DLLIMPORT extern
__OBJC_RW_DLLIMPORT void objc_msgSend(void);
__OBJC_RW_DLLIMPORT void objc_msgSendSuper(void);
__OBJC_RW_DLLIMPORT void objc_msgSend_stret(void);
__OBJC_RW_DLLIMPORT void objc_msgSendSuper_stret(void);
__OBJC_RW_DLLIMPORT void objc_msgSend_fpret(void);
__OBJC_RW_DLLIMPORT struct objc_class *objc_getClass(const char *);
__OBJC_RW_DLLIMPORT struct objc_class *class_getSuperclass(struct objc_class *);
__OBJC_RW_DLLIMPORT struct objc_class *objc_getMetaClass(const char *);
__OBJC_RW_DLLIMPORT void objc_exception_throw( struct objc_object *);
__OBJC_RW_DLLIMPORT void objc_sync_enter( struct objc_object *);
__OBJC_RW_DLLIMPORT void objc_sync_exit( struct objc_object *);
__OBJC_RW_DLLIMPORT Protocol *objc_getProtocol(const char *);
#ifndef __FASTENUMERATIONSTATE
struct __objcFastEnumerationState {
unsigned long state;
void **itemsPtr;
unsigned long *mutationsPtr;
unsigned long extra[5];
};
__OBJC_RW_DLLIMPORT void objc_enumerationMutation(struct objc_object *);
#define __FASTENUMERATIONSTATE
#endif
#ifndef __NSCONSTANTSTRINGIMPL
struct __NSConstantStringImpl {
int *isa;
int flags;
char *str;
long length;
};
#ifdef CF_EXPORT_CONSTANT_STRING
extern "C" __declspec(dllexport) int __CFConstantStringClassReference[];
#else
__OBJC_RW_DLLIMPORT int __CFConstantStringClassReference[];
#endif
#define __NSCONSTANTSTRINGIMPL
#endif
#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// Runtime copy/destroy helper functions (from Block_private.h)
#ifdef __OBJC_EXPORT_BLOCKS
extern "C" __declspec(dllexport) void _Block_object_assign(void *, const void *, const int);
extern "C" __declspec(dllexport) void _Block_object_dispose(const void *, const int);
extern "C" __declspec(dllexport) void *_NSConcreteGlobalBlock[32];
extern "C" __declspec(dllexport) void *_NSConcreteStackBlock[32];
#else
__OBJC_RW_DLLIMPORT void _Block_object_assign(void *, const void *, const int);
__OBJC_RW_DLLIMPORT void _Block_object_dispose(const void *, const int);
__OBJC_RW_DLLIMPORT void *_NSConcreteGlobalBlock[32];
__OBJC_RW_DLLIMPORT void *_NSConcreteStackBlock[32];
#endif
#endif
#define __block
#define __weak

#include
struct __NSContainer_literal {
void * *arr;
__NSContainer_literal (unsigned int count, ...) {
va_list marker;
va_start(marker, count);
arr = new void *[count];
for (unsigned i = 0; i < count; i++)
arr[i] = va_arg(marker, void *);
va_end( marker );
};
~__NSContainer_literal() {
delete[] arr;
}
};
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};

#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
#include

int main( int argc, const char *argv[] ) {
printf( "hello world\n" );
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

$ g++ -o Hello Hello.cpp
$ ./Hello
hello world

至於如何寫 Clang Rewriter 呢,就是直接參考 lib/Rewrite/RewriteObjC.cpp 這隻幾千行的程式碼吧!裡頭已經有豐富的資料可以參考,此例主要的有兩個 class :


class RewriteObjC : public ASTConsumer
class RewriteObjCFragileABI : public RewriteObjC


程式主體是 RewriteObjC,但使用時是用 RewriteObjCFragileABI:


ASTConsumer *clang::CreateObjCRewriter(const std::string& InFile, raw_ostream* OS, DiagnosticsEngine &Diags, const LangOptions &LOpts, bool SilenceRewriteMacroWarning) {
return new RewriteObjCFragileABI(InFile, OS, Diags, LOpts, SilenceRewriteMacroWarning);
}

一開始的程式進入點在 ASTConsumer constructor,隨後還有 virtual void Initialize(ASTContext &context),接著就跟 clang plugin example PrintFunctionNames 流程差不多,可以追 ASTConsumer 的 public functions 瞧瞧,如 virtual bool HandleTopLevelDecl(DeclGroupRef D) 等。


因此,撰寫的主體大概擺在 bool HandleTopLevelDecl(DeclGroupRef D) 裡頭,此例在 RewriteObjC 的宣告及 HandleTopLevelDecl 定義處:


// Top Level Driver code.
virtual bool HandleTopLevelDecl(DeclGroupRef D) {
for (DeclGroupRef::iterator I = D.begin(), E = D.end(); I != E; ++I) {
if (ObjCInterfaceDecl *Class = dyn_cast<ObjCInterfaceDecl>(*I)) {
if (!Class->isThisDeclarationADefinition()) {
RewriteForwardClassDecl(D);
break;
}
}

if (ObjCProtocolDecl *Proto = dyn_cast<ObjCProtocolDecl>(*I)) {
if (!Proto->isThisDeclarationADefinition()) {
RewriteForwardProtocolDecl(D);
break;
}
}

HandleTopLevelSingleDecl(*I);
}
return true;
}

另外一塊就是 void RewriteObjC::HandleTopLevelSingleDecl(Decl *D):


//===----------------------------------------------------------------------===//
// Top Level Driver Code
//===----------------------------------------------------------------------===//

void RewriteObjC::HandleTopLevelSingleDecl(Decl *D) {
//
//...
//

// If we have a decl in the main file, see if we should rewrite it.
if (SM->isFromMainFile(Loc)) // SM = SourceManager ; Loc = SourceLocation
return HandleDeclInMainFile(D);
}

別忘了瞧瞧 HandleDeclInMainFile 做了甚麼事囉!裡頭有豐富的 type checking 以及對應的處理機制,最後,則是文字取代的部分:


void ReplaceText(SourceLocation Start, unsigned OrigLength, StringRef Str) {
// If removal succeeded or warning disabled return with no warning.
if (!Rewrite.ReplaceText(Start, OrigLength, Str) || SilenceRewriteMacroWarning)
return;
Diags.Report(Context->getFullLoc(Start), RewriteFailedDiag);
}

如此一來,對 Clang Rewriter 的架構也熟悉了!


2012年5月24日 星期四

[OSX] 使用開機光碟安裝、重灌 Mac OS X Lion @ Mac mini

01

最近很忙,很久沒開啟 Mac mini 了,順便就把它升到 Lion 了。只是從 Mac Snow Leopard 10.6.8 升到 Lion 後,很多東西都保留了下來,對於大部分的人都很夠用,但我又開始在想,如果想要重灌該怎辦?這安裝方式不像以前購買 Mac mini 附的安裝光碟,可以從桌面上點選幾下就可以光碟開機、刪除硬碟資料並安裝 Mac OS X,所以,我找了一陣子才發現該怎樣光碟開機 :P 偏偏我的鍵盤是 PS/2 轉接 USB 後再加個 KVM,重開機的熱鍵少說也試了十多次吧 :P

關於 Mac OS X 開機熱鍵:Intel 型 Mac 開機組合鍵

實在是光碟開機一直沒成功,就改用 Startup Manager 方式,也就是開機時 + Option 鍵的方式(一般鍵盤是 ALT 鍵),試了幾次比較能抓住訣竅 XD 就是聽到開機聲音後馬上接著按(不要等音效播完),如此一來就能進去設定選單。

04

接著就接近以前 Snow Leopard 安裝光碟一樣,可以先把硬碟處理一下,再接著重裝 Lion 囉!

06

附帶一提,如果從 Startup Manager 進去,並點選 OS X Lion Recovery HD 進去,清完硬碟後,直接點選重新安裝 Lion 時,此時會要求透過網路從 App Store 下載,這時若有 USB 安裝方式的話,可以選擇重新選擇啟動硬碟並選擇安裝來源的媒介,這樣就可以透過 USB 或光碟來安裝了,就不需要重新從網路下載。

[OSX] Mac OS X Lion 軟體清單及偏好設定 @ Mac mini

Mac OS X Lion

前幾天裝了 Ubuntu 12.04 Desktop 版後,發現內鍵的桌布配色真不錯,雖然只是裝在 Virtualbox 裡頭,也令人感到清新。今晚就手癢也把 Mac mini 重灌完啦,神清氣爽。

每次安裝的軟體及設定越來越少了?不知算不算走回 command-line 的生活呢?


  • 文字編輯


    • 偏好設定 -> 預設純文字 && 字型大小 14

    • 保留在 Dock 上

  • 終端機


    • 偏好設定 -> 設定 -> Pro 預設 && 字型大小 14

    • 保留在 Dock 上

  • Yahoo! KeyKey (Yahoo!奇摩輸入法)


    • 系統偏好設定 -> 語言與文字 -> 輸入來源 -> 勾選 Yahoo 奇摩輸入法

  • Skype


    • 偏好 -> 音效影像 -> 麥克風&揚聲器 -> USB PnP Sound Devices (由於我多買一支 USB 音效卡,額外再設定 Skype 播放收音的位置)

    • 偏好 -> 警示 -> 細節 -> 聯絡人可接聽/聯絡人狀態轉為無法接通 -> 取消 播放音效 & 顯示內建的通知訊息

    • 偏好 -> 隱私 -> 取消 "允許 Skype 使用非個人身分資訊作為第三方廣告服務用途"

    • 保留在 Dock 上 & 登入自動啟動

  • Pidgin

  • Filezilla

  • Firefox


    • 工具 -> 附加元件 -> firebug、新同文堂

    • 檢視 -> 工具列 ->顯示書籤列、取消同文堂

    • 偏好 -> 一般 -> 設定首頁

    • 偏好 -> 個人隱私 -> 不被追蹤、不保留歷史紀錄

    • 安裝 Flash player

  • Google Chrome


    • 設定 -> 顯示首頁按鈕、顯示書籤列、設定首頁、取消翻譯閱讀網頁、取消記錄與詢問密碼

    • 擴充功能 -> 新同文堂、XMarks Bookmark Sync、Google 閱讀器通知程式 (由 Google 提供)、Google Mail Checker

2012年5月21日 星期一

Time Station

TimeStation


該怎麼說呢?開始感覺到時間的不夠用了,人生不像大學期中期末考前,一夜不睡,至少可以追到七八成。


Android 開發筆記 - 為自己的 app 建立 Content Provider

MyContentProvider


整個流程在 Android 官網(Create Your Content Provider)已經有很豐富的介紹,在此筆記一下 :P 加快自己往後回憶。


新增自己的 content provider 的用亦是供別人來存取自身 app 的資料(大多是 SQLite 操作),而實做流程並不難,但有一些小事要留意:



  1. 假設你的 app 的 package name 為 com.example.study,實做為 Content Provider Class 為 MyContentProvider。

  2. 首先,建立一個 app 名為 MyContentProvider,但不需要 Activity,新增完後,在新增一個 class 名為 MyContentProvider 並繼承 android.content.ContentProvider。

  3. 由於 content provider 給予外頭存取時,用到的格式為 "content://AUTHORITY/DBTable" 的型態,故決定 Content Provider 的 AUTHORITY 數值及使用的 SQLite db table name,此例定為 "com.example.study" 和 "MyTable1"、"MyTable2" 兩者。

  4. 實做 SQLiteOpenHelper,此例為 MyDBHelper,過程中建立兩張表,分別為 MyTable1 和 MyTable2 兩張表



    • CREATE TABLE MyTable1 ( _ID INTEGER PRIMARY KEY, _DATE DATETIME NULL )
      CREATE TABLE MyTable2 ( _ID INTEGER PRIMARY KEY, _DATA VARCHAR(50) NULL )




  5. 決定 content provider 所提供的 URI 服務後,使用 UriMatcher 來實做判斷方式



    • private static final UriMatcher mUriMatcher;
      private static final int URI_TYPE_TABLE1 = 1;
      private static final int URI_TYPE_TABLE2 = 2;
      static {
              mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
              mUriMatcher.addURI(AUTHORITY, MyDBHelper._DB_TABLE1, URI_TYPE_TABLE1);
              mUriMatcher.addURI(AUTHORITY, MyDBHelper._DB_TABLE2, URI_TYPE_TABLE2);
      }




  6. 實做 content provider 的相關函數,此例僅實做 String getType(Uri uri) 和 Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 兩個函數,前者用來告知 URI 所回傳的資料型態,後者則是 URI Query 時,將取得的 Cursor 操作方式(此為 SQLite 常用方式)

  7. 設定 AndroidManifest.xml,跟系統註冊使用 content provider。AndroidManifest.xml -> Application 分頁 -> Application Nodes -> Add -> Provider -> Name 欄位瀏覽 -> 選擇 MyContentProvider 後,並輸入 Authorities 欄位資訊(此例為com.example.study)

  8. 最後,實做一個 TestingActivity 來執行 content provider 的 query 動作,並從 Log.w 觀察情況。


程式碼:


MyContentProvider.java:


package com.example.study;


import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;


public class MyContentProvider extends ContentProvider {
        private static final String AUTHORITY = "com.example.study";
        private static final UriMatcher mUriMatcher;
        private static final int URI_TYPE_TABLE1 = 1;
        private static final int URI_TYPE_TABLE2 = 2;
        static {
                mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
                mUriMatcher.addURI(AUTHORITY, MyDBHelper._DB_TABLE1, URI_TYPE_TABLE1);
                mUriMatcher.addURI(AUTHORITY, MyDBHelper._DB_TABLE2, URI_TYPE_TABLE2);
        }
        private class MyDBHelper extends SQLiteOpenHelper {
                public static final String _DB_NAME = "MyDatabases.db";
                public static final String _DB_TABLE1 = "MyTable1";
                public static final String _DB_TABLE2 = "MyTable2";
                public static final int _DB_VERSION = 1;


                public MyDBHelper(Context context) {
                        super(context, _DB_NAME, null, _DB_VERSION);
                }


                public MyDBHelper(Context context, String name, CursorFactory factory, int version) {
                        super(context, name, factory, version);
                }


                @Override
                public void onCreate(SQLiteDatabase db) {
                        db.execSQL( "CREATE TABLE "+_DB_TABLE1+" ( _ID INTEGER PRIMARY KEY, _DATE DATETIME NULL )" );
                        db.execSQL( "CREATE TABLE "+_DB_TABLE2+" ( _ID INTEGER PRIMARY KEY, _DATA VARCHAR(50) NULL )" );
                }


                @Override
                public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                        db.execSQL("DROP TABLE IF EXISTS "+_DB_TABLE1);
                        db.execSQL("DROP TABLE IF EXISTS "+_DB_TABLE2);
                }
        }
        MyDBHelper mHelper = null;


        private void genTesting() {
                SQLiteDatabase mDB = mHelper.getWritableDatabase();

                // MyDBHelper._DB_TABLE1
                mDB.execSQL("INSERT INTO "+MyDBHelper._DB_TABLE1+" (_DATE) VALUES (datetime('now'))");


                // MyDBHelper._DB_TABLE2
                ContentValues values = new ContentValues();
                values.put("_DATA", "Hello World!");
                mDB.insertOrThrow(MyDBHelper._DB_TABLE2, null, values);


                mDB.close();
        }

        @Override
        public boolean onCreate() {
                // TODO Auto-generated method stub
                mHelper = new MyDBHelper(getContext());
                return true;
        }


        @Override
        public String getType(Uri uri) {
                // TODO Auto-generated method stub
                switch(mUriMatcher.match(uri)) {
                        case URI_TYPE_TABLE1:
                                return "vnd.android.cursor.item/vnd."+AUTHORITY+"."+MyDBHelper._DB_TABLE1; // one row
                        case URI_TYPE_TABLE2:
                                return "vnd.android.cursor.dir/vnd."+AUTHORITY+"."+MyDBHelper._DB_TABLE2; // multiple rows
                        default:
                                throw new IllegalArgumentException("Unknown URI " + uri);
                }
        }


        @Override
        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
                // TODO Auto-generated method stub
                genTesting();
                Cursor out = null;
                SQLiteDatabase mDB = null;
                switch(mUriMatcher.match(uri)) {
                        case URI_TYPE_TABLE1:
                                mDB = mHelper.getWritableDatabase();
                                out = mDB.rawQuery("SELECT * FROM "+MyDBHelper._DB_TABLE1+" ORDER BY _ID DESC LIMIT 1", null);
                                out.moveToFirst();
                                break;
                        case URI_TYPE_TABLE2:
                                mDB = mHelper.getWritableDatabase();
                                out = mDB.rawQuery("SELECT * FROM "+MyDBHelper._DB_TABLE2, null);
                                out.moveToFirst();
                                break;
                        default:
                                throw new IllegalArgumentException("Unknown URI " + uri);
                }
                return out;
        }


        @Override
        public Uri insert(Uri uri, ContentValues values) {
                // TODO Auto-generated method stub
                return null;
        }


        @Override
        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
                // TODO Auto-generated method stub
                return 0;
        }

        @Override
        public int delete(Uri arg0, String arg1, String[] arg2) {
                // TODO Auto-generated method stub
                return 0;
        }
}


MyContentProviderTestingActivity.java:


package com.example.study.test;


import android.app.Activity;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;


public class MyContentProviderTestingActivity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.main);

                Cursor c = getContentResolver().query(
                        Uri.parse("content://com.example.study/MyTable1"),
                        null,null,null,null ); // get all
                if( c != null ) {
                        c.moveToFirst();
                        while(c.isAfterLast()==false) {
                                Log.e("CP","MyTalbe1 field1:"+c.getString(0)+", field2:"+c.getString(1));
                                c.moveToNext();
                        }
                        c.close();

                        c = getContentResolver().query(
                                Uri.parse("content://com.example.study/MyTable2"),
                                null,null,null,null ); // get all
                        c.moveToFirst();
                        while(c.isAfterLast()==false) {
                                Log.e("CP","MyTalbe2 field1:"+c.getString(0)+", field2:"+c.getString(1));
                                c.moveToNext();
                        }
                        c.close();

                }
        }
}


2012年5月13日 星期日

回家走走吧~

2012-05-13


最近發生了一連串的事情,回家走走吧 :) 母親節快樂。


2012年5月9日 星期三

Android 開發筆記 - 使用 ListView 和 ListActivity

simple_list_item_1 simple_list_item_2


對 HTML 來說,所謂的 ListView 大概稱得上網頁上常看到的 ul、ol、dt 或是 table 吧!這東西真的是萬用的好工具,最直觀的排版,正所謂規規矩矩,不美也不會太醜。最近我終於開始在 Android 上使用這個東西了,光這功能就能讓你完成九成的工作了。更多 ListViewListActivity 的資訊,請參考官網的介紹,這邊僅簡易的筆記。


在 MyActivity 上呼叫自訂的 MyListActivity 的方式:


public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);


        Intent mIntent = new Intent();
        mIntent.setClass(this, MyListActivity.class);
        startActivity( mIntent );
}


在 MyListActivity 中,使用簡易的排版。範例一,使用系統內建 android.R.layout.simple_list_item_1 排版:


simple_list_item_1


 程式碼:


package com.example.study;


import android.app.ListActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;


public class MyListActivity extends ListActivity {
        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);

                String[] values = new String[] { "Android", "iPhone", "WindowsMobile", "Blackberry", "WebOS", "Ubuntu", "Windows7", "Max OS X", "Linux", "OS/2" };
                setListAdapter(
                        new ArrayAdapter<String>(
                                this, 
                                android.R.layout.simple_list_item_1,
                                values) 
                );
        }
}


範例二,使用系統內建 android.R.layout.simple_list_item_2 排版:


simple_list_item_2


程式碼:


package com.example.study;


import android.app.ListActivity;
import android.os.Bundle;
import java.util.ArrayList;
import java.util.HashMap;
import android.widget.SimpleAdapter;


public class ProgramListActivity extends ListActivity {
        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);


                final String ID_TITLE = "TITLE", ID_SUBTITLE = "SUBTITLE";

                ArrayList<HashMap<String,String>> myListData = new ArrayList<HashMap<String,String>>();
                String[] titles = new String[]{ "Title1" , "Title2", "Title3" };
                String[] subtitles = new String[]{ "SubTitle1" , "SubTitle2", "SubTitle3" };

                for( int i=0;i<titles.length ; ++i) {
                        HashMap<String,String> item = new HashMap<String,String>();
                        item.put(ID_TITLE,titles[i]);
                        item.put(ID_SUBTITLE,subtitles[i]);
                        myListData.add(item);
                }

                setListAdapter( new SimpleAdapter(
                        this,
                        myListData,
                        android.R.layout.simple_list_item_2,
                        new String[] { ID_TITLE, ID_SUBTITLE },
                        new int[] { android.R.id.text1, android.R.id.text2 } )
                );
        }
}


其中 android.R.layout.simple_list_item_1.xml 和 android.R.layout.simple_list_item_2.xml 的資料,可以查閱自己用的 SDK 囉,例如使用 Android 2.2 時,在 Eclipse 的 Package Explorer 中,可以瀏覽 Android 2.2 -> android.jar -> res.layout 並在裡頭找到。


此處提的範例都是 ListActivity 的,如果你在你的 Activity 的 layout 上擺了一個 ListView 的話,那只需要先用 findViewById 後,就可以仿照 ListActivity 把資料餵進去,如:


ListView mListView = (ListView)findViewById(R.id.listView1);
if( mListView != null ) {
        mListView.setAdapter(
                new SimpleAdapter(
                        this,
                        mDataSource ,
                        android.R.layout.simple_list_item_2,
                        new String[] { ID_TITLE, ID_DETAL },
                        new int[] { android.R.id.text1, android.R.id.text2 }
                )
        );
}


Android 開發筆記 - 使用 Content Provider


來源:Calendar Provider data model


由於安全設計關係,預設 app 操作使用的 sqlite db 都是在 sandbox 環境,換句話說,也就是僅 app 自身可獨立存取得而已,其他 app 是不能存取的。因此,如果想要提供自身的 sqlite db 資料給其他程式使用,除了寫 Android Service 架構來達到 IPC 或寫 Socket 程式溝通資料外,還可以採用 Content Provider 的架構。當然,在此不討論 root 後突破權限管理的機制 XD 舉例來說,Android 系統內建 Calendar app,而使用 Android 會綁定一個 Gmail 帳號,那有沒辦法取得此帳號的行事曆呢?此時可以透過 Content Provider 來進行操作。


首先,想要讀寫 Calendar 資料,那在 app 的 AndroidManifest.xml 權限上,需要加上 android.permission.READ_CALENDAR 和 android.permission.WRITE_CALENDAR,這樣的設計是告知使用者此程式將會存取 calendar 資料。


接著 app 中,可以透過 ContentResolver 物件,向 Calendar app 進行存取,如 content://CONTENT_URI/DBTable,此例為 Calendars.CONTENT_URI 等於 content://calendar/calendars,但建議使用 Calendars.CONTENT_URI 變數來存取,因為在各個 Android 版本中,可能因為軟體升級或架構改變而儲存位置變更,因此透過 Calendars.CONTENT_URI 存取最為恰當(有些版本為 content://com.android.calendar/calendars)。


片段程式碼:


// Projection array. Creating indices for this array instead of doing
// dynamic lookups improves performance.
public static final String[] EVENT_PROJECTION = new String[] {
        Calendars._ID, // 0
        Calendars.ACCOUNT_NAME, // 1
        Calendars.CALENDAR_DISPLAY_NAME, // 2
        Calendars.OWNER_ACCOUNT // 3
};

// The indices for the projection array above.
private static final int PROJECTION_ID_INDEX = 0;
private static final int PROJECTION_ACCOUNT_NAME_INDEX = 1;
private static final int PROJECTION_DISPLAY_NAME_INDEX = 2;
private static final int PROJECTION_OWNER_ACCOUNT_INDEX = 3;


// Run query
Cursor cur = null;
ContentResolver cr = getContentResolver();
Uri uri = Calendars.CONTENT_URI;
String selection = "((" + Calendars.ACCOUNT_NAME + " = ?) AND ("
        + Calendars.ACCOUNT_TYPE + " = ?) AND ("
        + Calendars.OWNER_ACCOUNT + " = ?))";
String[] selectionArgs = new String[] {"sampleuser@gmail.com","com.google","sampleuser@gmail.com"};
// Submit the query and get a Cursor object back.
cur = cr.query(uri, EVENT_PROJECTION, selection, selectionArgs, null);


此例 SQL 語法 = "SELECT Calendars._ID, Calendars.ACCOUNT_NAME, Calendars.CALENDAR_DISPLAY_NAME, Calendars.OWNER_ACCOUNT FROM Calendars WHERE Calendars.ACCOUNT_NAME = sampleuser@gmail.com AND Calendars.ACCOUNT_TYPE = com.google AND Calendars.OWNER_ACCOUNT = sampleuser@gmail.com";。


剩下的就跟操作 SQLite 一樣:


// Use the cursor to step through the returned records
while (cur.moveToNext()) {
        long calID = 0;
        String displayName = null;
        String accountName = null;
        String ownerName = null;

        // Get the field values
        calID = cur.getLong(PROJECTION_ID_INDEX);
        displayName = cur.getString(PROJECTION_DISPLAY_NAME_INDEX);
        accountName = cur.getString(PROJECTION_ACCOUNT_NAME_INDEX);
        ownerName = cur.getString(PROJECTION_OWNER_ACCOUNT_INDEX);

        // Do something with the values...


        ...
}


此外,如果要更新某一筆資料,也一樣透過 URI 來進行操作,僅需在最後加上 Record ID,如 content://CONTENT_URI/DBTable/RecordID,在進行資料的操作:


long calID = 2;
ContentValues values = new ContentValues();
// The new display name for the calendar
values.put(Calendars.CALENDAR_DISPLAY_NAME, "Trevor's Calendar");
Uri updateUri = ContentUris.withAppendedId(Calendars.CONTENT_URI, calID);
int rows = getContentResolver().update(updateUri, values, null, null);


此例 SQL 約略為 "UPDATE Calendars SET Calendars.CALENDAR_DISPLAY_NAME = Trevor's Calendar WHERE Calendars._ID = 2";。


當然也免不了新增資料:


long calID = 3;
long startMillis = 0;
long endMillis = 0;
Calendar beginTime = Calendar.getInstance();
beginTime.set(2012, 9, 14, 7, 30);
startMillis = beginTime.getTimeInMillis();
Calendar endTime = Calendar.getInstance();
endTime.set(2012, 9, 14, 8, 45);
endMillis = endTime.getTimeInMillis();
...
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
values.put(Events.DTSTART, startMillis);
values.put(Events.DTEND, endMillis);
values.put(Events.TITLE, "Jazzercise");
values.put(Events.DESCRIPTION, "Group workout");
values.put(Events.CALENDAR_ID, calID);
values.put(Events.EVENT_TIMEZONE, "America/Los_Angeles");
Uri uri = cr.insert(Events.CONTENT_URI, values);


// get the event ID that is the last element in the Uri
long eventID = Long.parseLong(uri.getLastPathSegment());
//
// ... do something with event ID
//
//


以上在 Android Developers 官網都有非常詳細的描述,在此僅簡易筆記一下,更多的 Content Provider 請參閱 package android.provider,裡頭可以看到系統內建有哪些資料可以存取使用囉!


Android 開發筆記 - 使用 SQLite / SQLiteDatabase / SQLiteOpenHelper

MyDBHelper


SQLite 這幾年來已經成了一種標準用法,無論是嵌入式或是 PC 上的軟體,很容易看的到 SQLite 的蹤跡了。


對 Android 系統架構我還沒那麼熟,粗略知道它有 SQLiteDatabase 和 SQLiteOpenHelper 兩個 class 可供 SQLite 操作使用。網路上翻到的資料,大概要先撰寫一隻 SQLiteOpenHelper,此角色是用來初始化資料庫,包含建 Table 、資料庫升級等對應動作。有些人習慣把資料庫的操作都寫在同一個 class (extends SQLiteOpenHelper)中,由於我經驗尚淺,我不曉得這樣的做法是不是正確的,因為觀看一些物件特性及機制似乎不太恰當。以下用簡易範例當作筆記。


新增 MyDBHelper 並繼承 android.database.sqlite.SQLiteOpenHelper,接著實作 void onCreate(SQLiteDatabase db) 和 void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) 後,就算完成 SQLiteOpenHelper 的工作了:


package com.example.study;


import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;


public class MyDBHelper extends SQLiteOpenHelper {
        final private static int _DB_VERSION = 1;
        final private static String _DB_DATABASE_NAME = "MyDatabases.db";
        public MyDBHelper(Context context) {
                super(context,_DB_DATABASE_NAME,null,_DB_VERSION);
        }
        public MyDBHelper(Context context, String name, CursorFactory factory, int version) {
                super(context, name, factory, version);
                // TODO Auto-generated constructor stub
        }


        @Override
        public void onCreate(SQLiteDatabase db) {
                // TODO Auto-generated method stub
                db.execSQL(
                        "CREATE TABLE MyTable (" +
                                " _ID INTEGER PRIMARY KEY, " +
                                " _DATA VARCHAR(50) NOT NULL, " +
                                " _DATETIME DATETIME NULL " +
                        ")"
                );
        }


        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                // TODO Auto-generated method stub
                db.execSQL("DROP TABLE IF EXISTS MyTable");
                onCreate(db);
        }
}


接著在 MyTestingActivity 中,就可以操作:


package com.example.study;


import android.app.Activity;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;


public class SQLiteDBTestingActivity extends Activity {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.main);

                // Init
                MyDBHelper mHelper = new MyDBHelper(this);
                SQLiteDatabase mDB = null;

                // Insert by raw SQL
                mDB = mHelper.getWritableDatabase();
                mDB.execSQL( "INSERT INTO MyTable (_DATA,_DATETIME) VALUES ('Hello World', datetime('now'))");
                mDB.close();

                // Insert by object method
                mDB = mHelper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("_DATA","YoHO");
                values.put("_DATETIME","2012-05-09 20:30:00");
                mDB.insertOrThrow("MyTable",null,values);
                mDB.close();

                // Query by raw SQL
                mDB = mHelper.getWritableDatabase(); // mDB = mHelper.getReadableDatabase();
                Cursor cursor = mDB.rawQuery("SELECT _ID, _DATA, _DATETIME FROM MyTable", null);
                cursor.moveToFirst();
                while(!cursor.isAfterLast()) {
                        Log.e("SQLiteDBTestingActivity","_ID = "+cursor.getInt(0));
                        Log.e("SQLiteDBTestingActivity","_DATA = "+cursor.getString(1));
                        Log.e("SQLiteDBTestingActivity","_DATETIME = "+cursor.getString(2).substring(0, 16));
                        cursor.moveToNext();
                }
                startManagingCursor(cursor);
                cursor.close();
                mDB.close();
        }
}


執行結果:


output


從上述片段程式來看,可以看到一開始 SQLiteOpenHelper 初始化必須傳入一個 Context,並且執行資料庫操作是透過 getWritableDatabase() 或 getReadableDatabase() 而來的,且查詢時會透過 Cursor 甚至 Activity 的 startManagingCursor 等動作,結束時還需做一些 close 的資源回收,因此,個人覺得把這一系列的動作都包進 SQLiteOpenHelper 似乎不太恰當。目前經驗尚淺,或許需要多累積一些實作經驗才會明瞭吧!


此外,可以透過 DDMS 將模擬器內的 SQLiteDB 取出來 (/data/data/com.example.study/databases/MyDatabases.db),並且用 sqlite3 等相仿的 cmd 去操作:


$ sqlite3 MyDatabases.db
SQLite version 3.6.22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .table
MyTable android_metadata
sqlite> SELECT * FROM MyTable;
1|Hello World|2012-05-09 12:41:07
2|YoHO|2012-05-09 20:30:00
sqlite>


2012年5月7日 星期一

[Linux] 免費編輯 PDF 文件 (刪除、新增頁面) @ Ubuntu 10.04

pdfeditor


最近偶爾會編輯一下 PDF 檔案,由於在 Windows 上似乎沒有幾套免費的 PDF 編輯軟體,有的免費的不支援存檔或是增減頁數,於是我只好默默開啟 Ubuntu 後,安裝 PDF Editor 啦 XD


$ sudo apt-get install pdfedit


接著就可以開啟想要更改的 pdf 檔案,例如把其中一頁給刪掉 (Remove Page)


remove page


如果編輯文件時,底下的視窗出現類似的資訊:


PDFedit
Loaded file : test.pdf
> removePageWithConditionalRefresh()
! In script '/usr/share/pdfedit/page.qs', line 118:
! Error. Exception in Pdf.removePage : Document is read-only


此時要先透過 [Tools] -> [Delinearize] 重新選這個檔案(此例為test.pdf),再另存起來(如 test2.pdf),接著開啟 test2.pdf 就能夠編輯。


pdfedit_add_pages


此外,也可以新增 pdf 檔案進來 [Tools] -> [Insert Pages from other document],接著開啟想要匯入的檔案後,可以把想要的 page 匯入,並調整頁數位置。雖然匯入的格式必須是 PDF 檔案,但也還算方便啦。


2012年5月5日 星期六

簡易拆解、組裝 iPhone 3G

15


幾個月前,同事的 iPhone 3GS 的電池開始出現充不飽、急速用完的問題,因此透過網拍管道找到換修電池的服務,當時跟他去看看到底怎樣拆 iPhone 3GS ,看完一遍後,就一直想要親手試看看,如今有空,我也拆了一台 iPhone 3G (不是 iPhone 3GS)或許哪天我也可以只買電池幫老家的 3G 手機換一下 :D (由於電池是貼黏在 iPhone 3G 背殼,所以此例只拆到看的見電池為此) 但我是建立在家中手機已經快 3 年的態度 XD 實在不建議其他人隨便看看文章就親手拆!這風險應該比 JB 還高吧 XDD 就當作個筆記笑笑吧!


老話一句:


拆機存在機器損壞風險,並喪失保固(貼紙損壞),請交給專業的
不要隨意嘗試,需自行承擔責任


01


首先是準備的工具,拆的話至少需要一個小吸盤跟螺絲起子,組裝的話需要多一支小夾子(那支尺只是比例參考)。我特地跑去五金賣場花了 32 元買了一支夠小的螺絲起子,實在是 iPhone 的螺絲大小約 2mm 而已!賣場還滿容易看到賣一盒小工具 110 ~ 210 的價錢,但我只需要最小的那支而已,且不能當場測試我也不知是不是夠小,去傳統五金賣場,還帶著 iPhone 去那邊試喔,這時才會發現傳統的美好 :D


02


拆掉接頭旁邊的兩個螺絲後,用手握手機的方式,把手握住靠近電源那頭,把吸盤擺在靠近插頭那端,須要給一點點力氣,就能夠使之分離。


03


此時螢幕與機身還有三處連結,這時可以看到一些標記處,這大概是提醒拆裝者的資訊。依編號來看,共有六個號碼,其中 1~3 個編號拆開後,就能使螢幕與機身分離,編號 4 應該是插頭與板子的連接處,編號5猜測是電池連接頭,編號6是一個圓形接頭,但我就沒研究了 XD


04


首先編號1算是很輕鬆就扳開,甚至可能把螢幕拉高一點就會自動分離,而編號2稍微緊了點,從下面扳開有點難,我就藉由它的帶子輕輕地分離它。


05 06 07


接著是很重要的編號3,這個接法算是此次最最麻煩且小心的地方,因為他就像兒時任天堂跟遊戲卡帶的接法,只是編號3是很柔的帶子,拆除是很簡單的,但若沒透過夾子,想要把它插回去是十分困難的,並且我隨意找的網路影片,都很自然地略過把它接回去的片段 orz 不知是不是要偷留一手。


08


當編號 1~3 拆開後,螢幕跟機身就分離了!


09 11


接著就順著流程把編號 4~6 都拆掉後,還要移除 sim 卡槽和右上角鏡頭的螺絲部分跟 7 處的小螺絲。


12


從右往左分別是兩個螢幕螺絲、鏡頭螺絲與 L 片,最後則是七顆小螺絲。


14 15


從插座那端慢慢地扳起來,接著網插座那端輕輕地抽出來,就可以看到電池啦。這次練習就到這裡而已,忘了先買電池,所以就沒繼續研究(因為電池是黏在背殼的,拆掉沒辦法黏的穩定)


20 21 22


接著就是組裝回去,一樣斜斜地把板子擺放到原處,記得要小心編號 6~4 不要被板子壓到。


23 24 25 26


接下來把可以鎖上編號五跟右下角的螺絲,將板子固定好,再來編號 4~6 、鏡頭擺好及 L 板鎖好。


27 28


如此一來,就只剩螢幕跟機身的部分了!最困難的地方就是要把編號 3 接好,當初研究很久,才想起來可以試試看夾子,透過夾子的幫忙,終於把編號3接回去了!大部分螢幕不會顯示,大概都是這處出錯吧 XD 其實我第一次也沒接好,因為沒用夾子,粗糙地接好但螢幕不會顯示!只能喃喃自語地說「反正這台過保也打算換了」,但內心卻在淌血 XDD 後來想到夾子後,在左手努力之下,終於按下開機按鍵可以看到白蘋果啦!


29


網路上可以看到很多討論串,大部分都說螢幕沒顯示的話,大概是把螢幕搞壞掉了 :p 起初我也小小放棄了,但後來還是覺得很怪,這種需要人力組裝的 3C 產品,在製程上應該是有 try 過的,雖然我是生手,不至於這麼容易壞(不然量產根本不可能吧?!),所以多方嘗試,我也把它救回來了 XD


2012年5月2日 星期三

OpenStreetMap API 筆記


From: Bing Maps Tile System


去年做了一整年跟圖資有關的案子,結果一直對這塊很好奇,原理大概知道,但細節卻遲遲沒播出時間來研究,今天毛起來翻了一下資料,就從 OpenStreetMap 下手吧!整體上原理就是把地球三維座標投影成二維座標,接著用一張張 256x256 正方型圖片來組成一張地圖,因此想要顯示地圖就是把目前關注的區域座標範圍,計算一下落在哪些圖上,接著把圖取出來即可。


此處 OpenStreetMap 的 Image API URL = "http://tile.openstreetmap.org/" + Zoom Level + "/" + X + "/" + Y + ".png" ,例如 Level 0 的地圖僅有一張,即為: http://tile.openstreetmap.org/0/0/0.png ,接著到 Level 1 時,變成 2x2 張圖、Level 2 為 4x4 張圖,每張圖仍維持 256x256 的大小,以此類推。


使用上就是要處理 GPS 座標:


在 Level 0 時,就是把座標轉換到 256x256 位置
在 Level 1 時,則為 512x512 位置
在 Level 2 時,計算 GPS 座標在 1024x1024 圖片的哪個位置
... 以此類推


至於座標投影如何轉換,可以參考 Bing Maps Tile System 或 Tiles à la Google Maps: Coordinates, Tile Bounds and Projection(globalmaptiles.py),也可以翻別人的 code 套公式,下次有空再來細細研究 XD


Level 0:








Level 0 

Leve 1:













Leve 1 (0,0)  Level 1 (1,0)
 Level 1 (0,1) Level 1 (1,1)

Leve 2:


 





























Level 2 (0,0) Level 2 (1,0) Level 2 (2,0)  Level 2 (3,0)
 Level 2 (0,1) Level 2 (1,1) Level 2 (2,1) Level 2 (3,1)
 Level 2 (0,2) Level 2 (1,2) Level 2 (2,2) Level 2 (3,2)
 Level 2 (0,3) Level 2 (1,3) Level 2 (2,3) Level 2 (3,3)

參考資料:


OpenStreetMap - Beginners Guide 1.5
OpenStreetMap - Slippy Map
OpenStreetMap - OpenLayers
OpenStreetMap - Google Maps Example
Bing Maps Tile System
Tiles à la Google Maps: Coordinates, Tile Bounds and Projection