2013-08-06 63 views
3

以前,這是不可能的(你必須全部用手寫出/創建一個靜態數組/將所有值放入字典並讀回它們......等)將Objective-C的enum常量轉換爲字符串名稱

但我注意到,最新的Xcode的lldb(4.6,也許更早的版本)自動將enum-constants轉換爲字符串。

我的問題是我們使用了很多庫 - 包括蘋果自己的! - 使用惱人的公共枚舉而不提供「價值到字符串」的方法。所以我最終不得不(很多很多次)做到這一點,「因爲庫先生的作者沒有這樣做,現在我必須爲他們製作靜態數組......」。

我一直希望蘋果能提供一個出路 - 它終於來了嗎?或者這只是調試器可以做的一些技巧 - 單純的運行時代碼無法訪問它?

+4

是的,只有調試器可以做到這一點 - 編譯器會生成調試符號,並使得字符串自動從枚舉中取出。另一種方法是'#define TO_STR(m)#m'預處理器宏,它可能會或可能不會令您滿意。 – 2013-08-06 18:46:23

回答

3

lldb沒有任何有關打印枚舉名稱的特殊功能。我認爲你所看到的是枚舉值被記錄在調試信息中(或不是)的結果。例如,

enum myenums {a = 0, b, c}; 
int main() 
{ 
enum myenums var = b; 
return (int) var; // break here 
} 

% xcrun clang -g a.c 
% xcrun lldb a.out 
(lldb) br s -p break 
Breakpoint 1: where = a.out`main + 18 at a.c:5, address = 0x0000000100000f92 
(lldb) r 
[...] 
-> 5  return (int) var; // break here 
    6 } 
(lldb) p var 
(myenums) $0 = b 
(lldb) p (myenums) 0 
(myenums) $1 = a 
(lldb) 

如果你看一下這個二進制文件(dwarfdump a.out.dSYM)的調試信息,你會看到的變量var類型是myenums和調試信息包括那些枚舉類型的值:

0x0000005a:  TAG_enumeration_type [5] * 
       AT_name("myenums") 
       AT_byte_size(0x04) 
       AT_decl_file("/private/tmp/a.c") 
       AT_decl_line(1) 

0x00000062:   TAG_enumerator [6] 
        AT_name("a") 
        AT_const_value(0x0000000000000000) 

0x00000068:   TAG_enumerator [6] 
        AT_name("b") 
        AT_const_value(0x0000000000000001) 

0x0000006e:   TAG_enumerator [6] 
        AT_name("c") 
        AT_const_value(0x0000000000000002) 

如果我再添枚舉未在任何地方使用我的樣本文件,

enum myenums {a = 0, b, c}; 
enum otherenums {d = 0, e, f}; // unused in this CU 
int main() 
{ 
enum myenums var = b; 
return (int) var; // break here 
} 

重新編譯,並期待在t他再次通過dwarfdump DWARF,我不會找到描述otherenums的任何調試信息 - 它是未使用的(在此編譯單元中),因此它被省略。

3

如前所述,除編譯器外,常量名稱在編譯後不可訪問。 H2CO3的TO_STR()宏的建議應該讓你通過大多數用途,我想。

我最近正在研究libclang,並決定通過解決您關於從枚舉值轉換爲字符串的苦惱的抱怨來運用它(「你必須全部用手寫出/創建一個靜態數組/將所有值轉換成字典並讀回來......等等)「。

這裏的一個Python腳本,將解析可可文件,找到枚舉(包括那些具有NS_OPTIONSNS_ENUM定義),和吐出任一陣列或功能(使用switch)做的映射。陣列選項採用稱爲"designated (array) initializers"一個C特徵,由此陣列成員可以與特定的索引來明確地配對的優點:

int arr[] = { [1] = 2, [3] = 8, [7] = 12 }; 

注意,這是不稀疏數組 - 前的任何非指定的索引最後仍然是被創建的,並且初始化爲0.其含義是,打算用作位掩碼的枚舉值,其值爲1 << 2,1 << 3,1 << 4等,將爲相對較少使用的值創建相當大的數組。在這些情況下,函數可能是更好的選擇。

匿名enum s(我認爲它們都是單人成員,至少在Foundation中)使用常量的名稱直接轉換爲單個NSString。我已經對負值的常量進行了自動處理,例如NSOrderedAscending - 數組初始化器中的負索引不會編譯,但函數/ switch替代方法可以正常工作;您只需爲這些少數情況手動選擇即可。

腳本是up on GitHub,這裏是完整的。麻省理工學院的許可證,所以你喜歡做什麼。我很樂意聽到任何修改。

#!/usr/bin/env python 

""" 
CocoaEnumToString 

Parse a specified Cocoa header file and emit ObjC code to translate the values 
of any enums found within into their names as NSStrings. 
""" 
# Copyright 2013 Joshua Caswell. 
# Permission is hereby granted, free of charge, to any person obtaining a copy 
# of this software and associated documentation files (the "Software"), to deal 
# in the Software without restriction, including without limitation the rights 
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
# copies of the Software, and to permit persons to whom the Software is 
# furnished to do so, subject to the following conditions: 
# 
# The above copyright notice and this permission notice shall be included in 
# all copies or substantial portions of the Software. 
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
# THE SOFTWARE. 

import itertools 
import argparse 
import sys 
import re 
from os import path 

from clang import cindex 
from clang.cindex import CursorKind 

def all_children(node): 
    return itertools.chain(iter([node]), *map(all_children, 
               node.get_children())) 

def all_constant_decls(enum): 
    return iter(child for child in all_children(enum) if 
        child.kind == CursorKind.ENUM_CONSTANT_DECL) 

def indent_all_lines(s, indent): 
    return '\n'.join(indent + line for line in s.split('\n')) 

def format_anonymous(enum, title): 
    const_str = 'NSString * const {} = @"{}";\n' 
    constants = [const_str.format(title.replace('%e', constant.spelling), 
            constant.spelling) 
          for constant in all_constant_decls(enum)] 
    return "".join(constants) 


def format_as_array(enum, title, indent): 
    all_members = ['[{0}] = @"{0}"'.format(constant.spelling) 
         for constant in all_constant_decls(enum)] 
    all_members = ",\n".join(all_members) 

    title = title.replace('%e', enum.spelling) 
    array_str = "NSString * const {}[] = {{\n{}\n}};" 
    return array_str.format(title, indent_all_lines(all_members, indent)) 

def format_as_func(enum, title, indent): 
    case_str = 'case {0}:\n{1}return @"{0}";' 
    all_cases = [case_str.format(constant.spelling, indent) 
         for constant in all_constant_decls(enum)] 
    all_cases.append('default:\n{}@"";'.format(indent)) 
    all_cases = "\n".join(all_cases) 

    switch = "switch(val){{\n{}\n}}".format(indent_all_lines(all_cases, 
                   indent)) 
    title = title.replace('%e', enum.spelling) 
    func_str = "NSString * {}({} val){{\n{}\n}}" 
    return func_str.format(title, enum.spelling, 
          indent_all_lines(switch, indent)) 


parser = argparse.ArgumentParser(description="Use libclang to find enums in " 
            "the specified Objective-C file and emit a " 
            "construct (array or function) that " 
            "maps between the constant values and " 
            "their names.") 
# This argument must be added to the parser first for its default to override 
# that of --arr and --fun 
parser.add_argument("-c", "--construct", default="array", 
        help="Specify 'function' or any prefix ('f', 'fun', etc.) " 
         "to emit a function that uses a switch statement for " 
         "the mapping; specify 'array' or any prefix for " 
         "an array (this is the default). Whichever of -c, " 
         "--arr, or --fun occurs last in the argument list " 
         "will dictate the output.") 
parser.add_argument("--arr", "--array", action="store_const", const="array", 
        dest="construct", help="Emit an array for the mapping.") 
parser.add_argument("-e", "--enums", action="append", 
        help="Specify particular enums to capture; by default " 
        "all enums in the given file are used. This argument may " 
        "be present multiple times. Names which are not found in " 
        "the input file are ignored.") 
parser.add_argument("--fun", "--func", "--function", action="store_const", 
        const="function", dest="construct", 
        help="Emit a function for the mapping.") 
parser.add_argument("-i", "--indent", default="4s", 
        help="Number and type of character to use for indentation." 
        " Digits plus either 't' (for tabs) or 's' (for spaces), " 
        "e.g., '4s', which is the default.") 
parser.add_argument("-n", "--name", default="StringFor%e", 
        help="Name for the construct; the prefix will " 
# Escape percent sign because argparse is doing some formatting of its own. 
        "be added. Any appearances of '%%e' in this argument will " 
        "be replaced with each enum name. The default is " 
        "'StringFor%%e'.") 
parser.add_argument("-o", "--output", 
        help="If this argument is present, output should go to a " 
        "file which will be created at the specified path. An " 
        "error will be raised if the file already exists.") 
parser.add_argument("-p", "--prefix", default="", 
        help="Cocoa-style prefix to add to the name of emitted " 
        "construct, e.g. 'NS'") 
parser.add_argument("file", help="Path to the file which should be parsed.") 


arguments = parser.parse_args() 

if "array".startswith(arguments.construct): 
    format_enum = format_as_array 
elif "function".startswith(arguments.construct): 
    format_enum = format_as_func 
else: 
    parser.error("Neither 'function' nor 'array' specified for construct.") 

match = re.match(r"(\d*)([st])", arguments.indent) 
if not match.group(2): 
    parser.error("Neither tabs nor spaces specified for indentation.") 
else: 
    indent_char = '\t' if match.group(2) == 't' else ' ' 
    indent = indent_char * int(match.group(1) or 1) 

if arguments.output: 
    if path.exists(arguments.output): 
     sys.stderr.write("Error: Requested output file exists: " 
         "{}\n".format(arguments.output)) 
     sys.exit(1) 
    else: 
     out_f = open(arguments.output, 'w') 
else: 
    out_f = sys.stdout 

target_file_name = arguments.file 


# Ignore the fact that system libclang probably doesn't match the version 
# of the Python bindings. 
cindex.Config.set_compatibility_check(False) 
# Use what's likely to be a newer version than that found in /usr/lib 
cindex.Config.set_library_file("/Applications/Xcode.app/Contents/Developer/" 
           "Toolchains/XcodeDefault.xctoolchain/usr/lib/" 
           "libclang.dylib") 

# Preprocessor macros that resolve into enums; these are defined in 
# NSObjCRuntime.h, but including that directly causes redefinition errors due 
# to it being also imported. 
ns_options_def = ("NS_OPTIONS(_type, _name)=enum _name : " 
       "_type _name; enum _name : _type") 
ns_enum_def = ("NS_ENUM(_type, _name)=enum _name : _type _name; " 
       "enum _name : _type") 

tu = cindex.TranslationUnit.from_source(target_file_name, 
             args=["-ObjC", "-D", ns_enum_def, 
               "-D", ns_options_def, "-D", 
               "NS_ENUM_AVAILABLE="]) 


enums = [node for node in all_children(tu.cursor) if 
       node.kind == CursorKind.ENUM_DECL and 
       node.location.file.name.find(target_file_name) != -1] 
if arguments.enums: 
    enums = filter(lambda enum: enum.spelling in arguments.enums, enums) 

title = arguments.prefix + arguments.name 

for enum in enums: 
    if not enum.spelling: 
     out_f.write(format_anonymous(enum, title)) 
    else: 
     out_f.write(format_enum(enum, title, indent)) 
    out_f.write("\n\n")