Skip to content

Interoperability

Kyunggook Kim edited this page Apr 11, 2017 · 2 revisions
  • Swift의 가장 큰 장점 중 하나는 C 및 Objective-C와 상호 운용 할 때 낮은 마찰

  • Swift는 Objective-C 타입을 기본 Swift 타입에 자동으로 연결(Bridge)할 수 있으며 많은 C 유형과도 연결

  • 이렇게 하면 기존 라이브러리를 사용하고 멋진 인터페이스를 제공

  • 이 장에서는 CommonMark 라이브러리에 대한 랩퍼를 작성

  • CommonMark는 일반 텍스트 서식 지정에 널리 사용되는 구문인 Markdown의 공식 사양

  • GitHub 또는 Stack Overflow에서 게시물을 작성한 적이 있다면 아마도 Markdown을 사용했을 것

  • 이 실제적인 예제 후에 표준 라이브러리가 메모리 작업을 위해 제공하는 도구를 살펴보고 C 코드와 상호 작용하는 데 사용되는 방법을 보자

Hands-On: Wrapping CommonMark

  • C 코드로 호출 할 수 있는 Swift의 기능을 사용하면 풍부한 C 라이브러리를 활용

  • Swift에서 기존 라이브러리의 인터페이스를 감싸는 래퍼를 작성하는 것은 처음부터 무언가를 만드는 것보다 훨씬 쉽고 적은 작업을 필요

  • 한편 래퍼 사용자는 완전히 원시 솔루션에 비해 형식 안전성이나 사용 편의성면에서 차이가 없음

  • 동적 라이브러리와 헤더 파일만 있으면 됨

  • 이 예제 인 CommonMark C 라이브러리는 빠르고 잘 테스트된 CommonMark 스펙의 참조 구현

  • Swift에서 CommonMark에 액세스 할 수 있도록 계층화된 접근 방식을 취할 것

  • 먼저, 라이브러리가 노출하는 불투명 한 유형을 중심으로 매우 얇은 Swift 클래스를 생성

  • 그런 다음 우리는보다 관용적인 API를 제공하기 위해이 클래스를 Swift enum으로 래핑

Wrapping the C Library

  • 보다 나은 인터페이스로 단일 함수를 래핑하는 것으로 시작
  • cmark_markdown_to_html 함수는 Markdown 형식의 텍스트를 가져 와서 결과 HTML 코드를 문자열로 반환
  • C 인터페이스는 다음과 같음
/** Convert 'text' (assumed to be a UTF-8 encoded string with length
* 'len') from CommonMark Markdown to HTML, returning a null-terminated, * UTF-8-encoded string.
*/

char *cmark_markdown_to_html(const char *text, size_t len, int options);
  • Swift가 이 선언을 가져 오면 첫 번째 매개 변수에 있는 C 문자열을 여러 Int8 값의 UnsafePointer로 표시
  • 문서에서 UTF-8 코드 단위가 될 것으로 예상되고 len 매개 변수는 문자열의 길이
func cmark_markdown_to_html
  (_ text: UnsafePointer<Int8>!, _ len: Int, _ options: Int32)
  -> UnsafeMutablePointer<Int8>!
  • 물론 래퍼 함수가 Swift 문자열로 작업하기를 원하기 때문에 Swift 문자열을 Int8 포인터로 변환해야한다고 생각할 수도 있지만
  • 기본 문자열과 C 문자열 간의 연결은 Swift가 자동으로 수행하는 일반적인 작업
  • 함수는 UTF-8로 인코딩 된 문자열의 길이를 바이트 단위로 예상하고 문자 수는 기대하지 않으므로 len 매개 변수에 주의
  • 우리는 문자열의 utf8 뷰에서 올바른 값을 얻고 옵션에 대해 0을 전달
func markdownToHtml(input: String) -> String {
  let outString = cmark_markdown_to_html(input, input.utf8.count, 0)!
  return String(cString: outString)
}
  • 우리가 함수가 반환하는 문자열 포인터를 강제적으로 언래핑한다는 것을 주목

  • cmark_markdown_to_html이 항상 유효한 문자열을 반환한다는 것을 알고 있으므로 안전하게 이 작업을 수행

  • 메소드 내에서 force-unwrapping을 사용하면, 라이브러리 사용자는 optionals에 대해 걱정할 필요없이

  • markdownToHTML 메소드를 호출하고 결과는 결코 어쨌든 사라지지 않음

  • 네이티브 Swift 문자열을 C 문자열로 자동 브리징하면 호출할 C 함수에서 문자열이 UTF-8로 인코딩되어야 한다고 가정

  • 이것은 대부분의 경우 올바른 선택이지만 C API가 다른 인코딩을 사용한다고 가정하면 자동 브리징을 사용할 수 없지만 그것은 종종 아직도 쉬움

  • 예를 들어, UTF-16 코드 포인트의 배열이 필요한 경우 Array(string.utf16)를 사용

Wrapping the cmark_node Type

  • 스트레이트 HTML 출력 외에도, cmark 라이브러리는 Markdown 텍스트를 구조화된 트리 요소로 구문 분석하는 방법을 제공

  • 예를 들어 간단한 텍스트를 단락, 따옴표, 목록, 코드 블록, 머리글 등과 같은 블록 수준 노드의 목록으로 변환

  • 일부 블록 수준 요소는 다른 블록 수준 요소 (예 : 따옴표에는 여러 단락을 포함 할 수 있음)를 포함하는 반면 다른 요소는 인라인 요소만 포함(예 : 헤더에 강조 부분을 포함 할 수 있음)

  • 요소는 둘 다 포함 할 수 없음(예 : 목록 항목의 인라인 요소는 항상 단락 요소에 래핑)

  • C 라이브러리는 단일 데이터 유형 cmark_node를 사용하여 노드를 나타내는데 불투명한데 라이브러리의 작성자는 그 정의를 숨기려고 함

  • 헤더에서 볼 수있는 것은 cmark_node에 대한 포인터를 조작하거나 반환하는 함수

  • Swift는 이러한 포인터를 OpaquePointers로 가져옴(이 장의 뒷부분에 있는 OpaquePointerUnsafeMutablePointer와 같은 표준 라이브러리의 여러 포인터 유형 간의 차이점을 자세히 보자)

  • 앞으로 Swift의 "멤버로 가져 오기" 기능을 사용하여 이러한 함수를 cmark_node의 메소드로 가져옴

  • Apple은 이를 사용하여 Swift의 Core Graphics 및 Grand Central Dispatch에 더 많은 "객체 지향"API를 제공

  • C 소스 코드에 swift_name 속성을 사용하여 주석을 달아 작동하기 원하지만 이 기능은 아직 cmark 라이브러리에서 실제로 작동하지 않음

  • 자신의 C 라이브러리에서는 작동할 수도 있는데 멤버로서 import에 관한 버그가 존재

  • 예를 들어, 그것은 항상 불투명 포인터 (정확히 cmark에서 사용되는 것)에서 작동하지 않음

  • 네이티브 스위프트 유형으로 노드를 포장하여 작업하기 쉽도록 함

  • 구조체와 클래스에 대한 장에서 보았듯이 사용자 정의 유형을 만들 때마다 값 의미(value semantics)에 대해 생각해 볼 필요가 있음

  • 유형의 값인가, 아니면 인스턴스가 ID를 갖는 것이 합리적인가?

  • 전자의 경우 구조체 또는 열거형을 선호해야하지만 후자는 클래스가 필요

  • 우리의 경우는 흥미로운데 Markdown 문서의 노드는 값

  • 동일한 요소 유형과 내용을 가진 노드 두 개를 구별 할 수 없으므로 ID가 없어야 함

  • 반면, 우리는 cmark_node의 내부 구조를 알지 못하기 때문에 노드의 복사본을 만드는 직접적인 방법이 없으므로 값의 의미를 보장 할 수 없음

  • 이러한 이유로 우리는 클래스부터 시작

  • 나중에 이 클래스의 최상단에 또 다른 레이어를 작성하여 값 의미 체계와 인터페이스를 제공

  • 우리 클래스는 단순히 불투명 포인터를 저장하고 이 클래스의 인스턴스에 참조가 남아 있지 않을 때 deactivate에서 cmark_node 메모리를 해제

  • 우리는 문서 수준에서 메모리를 비우는데 그렇지 않으면 우리는 여전히 사용중인 노드를 비울 수 있기 때문

  • 문서를 비우면 자동으로 모든 자식을 재귀적으로 해제

  • 이런 식으로 불투명 포인터를 감싸면 자동 참조 카운팅이 해결함

public class Node {
  let node: OpaquePointer

  init(node: OpaquePointer) {
    self.node = node
  }

  deinit {
    guard type == CMARK_NODE_DOCUMENT else { return }
    cmark_node_free(node)
  }
}
  • 다음 단계는 Markark 텍스트를 파싱하고 문서의 루트 노드를 반환하는 cmark_parse_document 함수를 래핑하는 것
  • cmark_markdown_to_html과 같은 인수를 취함 : 문자열, 길이 및 구문 분석 옵션을 설명하는 정수
  • Swift에서 cmark_parse_document 함수의 반환 유형은 노드를 나타내는 OpaquePointer
func cmark_parse_document
  (_ buffer: UnsafePointer<Int8>!, _ len: Int, _ options: Int32)
  -> OpaquePointer!
  • 우리는 이 함수를 우리 클래스의 convenience initializer로 교체
  • 구문 분석에 실패하면이 함수는 nil을 반환
  • 따라서 우리의 초기화 프로그램은 실패할 수 있고, 만약 이것이 발생한다면 nil(널 포인터가 아닌 옵셔널 값)을 반환
public init?(markdown: String) {
  let parsed = cmark_parse_document(markdown, markdown.utf8.count, 0)
  guard let node = parsed else { return nil }
  self.node = node
}
  • 위에서 언급했듯이 노드에서 작동하는 몇 가지 흥미로운 기능, 예를 들어, 단락이나 머리글과 같은 노드 유형을 반환하는 메소드
cmark_node_type cmark_node_get_type(cmark_node *node);
  • 스위프트에서는 다음과 같음
func cmark_node_get_type(_ node: OpaquePointer!) -> cmark_node_type
  • cmark_node_type은 다양한 블록 수준 및 인라인의 경우가있는 C 열거형
  • Markdown에 정의 된 요소는 물론 오류를 나타내는 경우도 있음
typedef enum {
  /* Error status */
  CMARK_NODE_NONE,

  /* Block */
  CMARK_NODE_DOCUMENT,
  CMARK_NODE_BLOCK_QUOTE,
  ...

  /* Inline */
  CMARK_NODE_TEXT,
  CMARK_NODE_EMPH,
  ...
} cmark_node_type;
  • Swift는 일반 C 열거형을 Int32만 포함하는 구조체로 가져옴
  • 또한 열거형의 모든 경우에 대해 최상위 수준 변수가 생성
  • Apple이 Objective-C 프레임 워크에서 사용하는 NS_ENUM 매크로로 표시된 열거형은 기본 Swift 열거형으로 가져옴
  • 또한 swift_name을 사용하여 enum 사례에 주석을 달아 멤버 변수로 만들 수 있음
struct cmark_node_type : RawRepresentable, Equatable {
  public init(_ rawValue: UInt32)
  public init(rawValue: UInt32)
  public var rawValue: UInt32
}

var CMARK_NODE_NONE: cmark_node_type { get }
var CMARK_NODE_DOCUMENT: cmark_node_type { get }
  • Swift에서 노드 유형은 Node 데이터 타입의 프로퍼티이어야 하므로 cmark_node_get_type 함수를 클래스의 컴퓨티드 프로퍼티로 바꿈
var type: cmark_node_type {
  return cmark_node_get_type(node)
}
  • 이제 node.type을 작성하여 요소 유형을 얻을 수 있음
  • 우리가 액세스 할 수 있는 노드 속성이 두 개 더 있는데 예를 들어 노드가 목록인 경우 글 머리 기호 또는 정렬 된 두 가지 목록 유형 중 하나를 가짐
  • 다른 모든 노드는 목록 유형이 no list
  • Swift는 해당 C 열거형을 구조체로 나타내며 각 사례에 최상위 변수가 있으며 비슷한 래퍼 속성을 쓸 수 있음
  • 이 경우에는 이 장의 뒷부분에서 편리하게 사용할 수있는 setter도 제공
var listType: cmark_list_type {
  get { return cmark_node_get_list_type(node) }
  set { cmark_node_set_list_type(node, newValue) }
}
  • 다른 모든 노드 특성 (예 : 머리글 레벨, 분리 코드 블록 정보, 링크 URL 및 제목)에 대해 유사한 기능

  • 이러한 속성은 특정 유형의 노드에만 적용되는 경우가 많으며 선택적 인터페이스 (예 : 링크 URL) 또는 기본값 (예 : 기본 헤더 수준이 0)으로 인터페이스를 제공하도록 선택

  • 이것은 Swift에서 더 잘 모델링 할 수 있는 라이브러리의 C API의 주요 약점을 보여줌

  • 일부 노드에는 하위 노드가 있을 수도 있음

  • 이들을 반복하기 위해 CommonMark 라이브러리는 cmark_node_rst_childcmark_node_next 함수를 제공

  • Node 클래스가 그 자식 배열을 제공하기를 바람

  • 이 배열을 생성하기 위해 첫 번째 자식부터 시작하여 cmark_node_rst_child 또는 cmark_node_next가 nil을 반환 할 때까지 자식을 계속 추가하여 목록의 끝을 알림

  • 이 C 널 포인터는 자동으로 옵셔널로 변환

var children: [Node] {
  var result: [Node] = []
  var child = cmark_node_ rst_child(node)
  while let unwrapped = child {
    result.append(Node(node: unwrapped))
    child = cmark_node_next(child)
  }
  return result
}
  • 배열이 아닌 지연 시퀀스를 반환하도록 선택할 수도 있음 (예 : 시퀀스 또는 AnySequence 사용)

  • 그러나 이 문제는 노드 구조가 시퀀스의 생성과 소비 사이에서 변경 될 수 있음

  • 이 경우 아래 코드는 잘못된 값을 반환하거나 더 심하게 충돌

  • 유즈 케이스에 따라 느리게 생성된 시퀀스를 반환하는 것은 원하는 것일 수 있지만 데이터 구조가 변경될 수 있는 경우 배열을 반환하는 것이 훨씬 안전한 선택

  • 노드를 위한 이 간단한 래퍼 클래스를 사용하면 Swift에서 CommonMark 라이브러리로 생성된 추상 구문 트리에 액세스하는 것이 훨씬 쉬워짐

  • cmark_node_get_list_type과 같은 함수를 호출 할 필요없이 node.listType을 작성하고 자동 완성과 안전을 입력할 수 있지만 아직 완료되지 않음

  • Node 클래스가 C 함수보다 훨씬 더 원시적이라고 느낄지라도 Swift는 관련 값을 가진 enum을 사용하여 더 자연스럽고 안전한 방법으로 노드를 표현

A Safer Interface

  • 위에서 언급했듯이 특정 컨텍스트에서만 적용되는 많은 노드 속성이 있음
  • 예를 들어, 목록의 headerLevel이나 코드 블록의 listType에 액세스하는 것은 의미가 없음
  • associated values이 있는 열거형을 사용하면 특정 사례별로 의미가있는 메타 데이터만 지정
  • 가능한 모든 인라인 요소에 대해 하나의 열거형을 만들고 블록 수준의 항목에 대해 또 다른 열거형을 만듦
  • 그렇게 하면 CommonMark 문서의 구조를 적용
  • 예를 들어 일반 텍스트 요소는 String을 저장하지만 강조 노드는 다른 인라인 요소의 배열을 포함
  • 이 열거형은 우리 라이브러리에 대한 공용 인터페이스가 될 것이고 Node 클래스를 내부 구현 세부 사항으로 바꿀 것
public enum Inline {
  case text(text: String)
  case softBreak
  case lineBreak
  case code(text: String)
  case html(text: String)
  case emphasis(children: [Inline])
  case strong(children: [Inline])
  case custom(literal: String)
  case link(children: [Inline], title: String?, url: String?)
  case image(children: [Inline], title: String?, url: String?)
}
  • 마찬가지로 단락과 헤더에는 인라인 요소만 포함될 수 있지만 블록 인용에는 항상 다른 블록 수준 요소가 포함됨
  • 목록은 목록 항목을 나타내는 Block 요소의 배열로 정의됨
public enum Block {
  case list(items: [[Block]], type: ListType)
  case blockQuote(items: [Block])
  case codeBlock(text: String, language: String?)
  case html(text: String)
  case paragraph(text: [Inline])
  case heading(text: [Inline], level: Int)
  case custom(literal: String)
  case thematicBreak
}
  • ListType은 목록이 정렬되었는지 또는 정렬되지 않은지 여부를 나타내는 단순한 열거형
public enum ListType {
  case Unordered
  case Ordered
}
  • 열거형은 값 유형이므로 이 또한 열거형 표현으로 변환하여 노드를 값으로 처리

  • API는 형식 변환을 위한 초기화를 사용하여 API 디자인 지침을 따름

  • 두 쌍의 초기화를 작성, 한 쌍은 노드 유형에서 Block 및 InlineElement 값을 작성하고, 다른 쌍은 이 열거 형에서 Node를 재구성

  • 이를 통해 InlineElement 또는 Block 값을 변형하고 CommonMark 문서를 재구성하여 HTML, 맨 페이지 또는 Markdown 텍스트로 다시 렌더링 할 수있는 함수를 작성

  • 먼저 노드를 InlineElement로 변환하는 초기화를 작성

  • 노드 유형을 켜고 해당 인라인 값을 구성

  • 예를 들어, 텍스트 노드의 경우, 우리는 cmark 라이브러리의 리터럴 속성을 통해 액세스하는 노드의 문자열 내용을 가져옴

  • 텍스트 노드는 항상이 값을 가지지만 다른 노드 유형은 리터럴에 대해 nil 값을 가질 수 있기 때문에 리터럴을 안전하게 해제

  • 예를 들어, 강조 및 강력한 노드는 자식 노드 만 있고 리터럴 값은 없음

  • 후자를 파싱하기 위해 노드의 자식을 매핑하고 초기화 프로그램을 재귀적으로 호출

  • 이 코드를 복제하는 대신 필요한 경우에만 호출되는 인라인 함수 인 inlineChildren을 만듦

  • 기본 경우에는 절대로 도달하지 않아야하므로 프로그램을 트랩하도록 선택

  • 이것은 옵션 또는 throw를 반환하는 것은 일반적으로 예상 오류에만 사용되어야하며 프로그래머 오류를 나타내지 않아야한다는 규칙을 따름

extension Inline {
  init(_ node: Node) {
    let inlineChildren = { node.children.map(Inline.init) }
    switch node.type {
    case CMARK_NODE_TEXT:
      self = .text(text: node.literal!)
    case CMARK_NODE_SOFTBREAK:
      self = .softBreak
    case CMARK_NODE_LINEBREAK:
      self = .lineBreak
    case CMARK_NODE_CODE:
      self = .code(text: node.literal!)
    case CMARK_NODE_HTML_INLINE:
      self = .html(text: node.literal!)
    case CMARK_NODE_CUSTOM_INLINE:
      self = .custom(literal: node.literal!)
    case CMARK_NODE_EMPH:
      self = .emphasis(children: inlineChildren())
    case CMARK_NODE_STRONG:
      self = .strong(children: inlineChildren())
    case CMARK_NODE_LINK:
      self = .link(children: inlineChildren(), title: node.title, url: node.urlString)
    case CMARK_NODE_IMAGE:
      self = .image(children: inlineChildren(), title: node.title, url: node.urlString)
    default:
      fatalError("Unrecognized node: \(node.typeString)")
    }
  }
}
  • 블록 레벨 요소를 변환하는 것은 동일한 패턴을 따름
  • 블록 레벨 요소는 노드 유형에 따라 인라인 요소, 목록 항목 또는 기타 블록 수준 요소를 자식으로 가질 수 있음
  • cmark_node 구문 트리에서 목록 항목은 추가 노드로 랩핑됨
  • Node의 listItem 속성에서 해당 레이어를 제거하고 블록 수준 요소의 배열을 직접 반환
extension Block {
  init(_ node: Node) {
    let parseInlineChildren = { node.children.map(Inline.init) }
    let parseBlockChildren = { node.children.map(Block.init) }
    switch node.type {
    case CMARK_NODE_PARAGRAPH:
      self = .paragraph(text: parseInlineChildren())
    case CMARK_NODE_BLOCK_QUOTE:
      self = .blockQuote(items: parseBlockChildren())
    case CMARK_NODE_LIST:
      let type = node.listType == CMARK_BULLET_LIST ? ListType.Unordered : ListType.Ordered
      self = .list(items: node.children.map { $0.listItem }, type: type)
    case CMARK_NODE_CODE_BLOCK:
      self = .codeBlock(text: node.literal!, language: node.fenceInfo)
    case CMARK_NODE_HTML_BLOCK:
      self = .html(text: node.literal!)
    case CMARK_NODE_CUSTOM_BLOCK:
      self = .custom(literal: node.literal!)
    case CMARK_NODE_HEADING:
      self = .heading(text: parseInlineChildren(), level: node.headerLevel)
    case CMARK_NODE_THEMATIC_BREAK:
      self = .thematicBreak
    default:
      fatalError("Unrecognized node: \(node.typeString)")
    }
  }
}
  • 이제 문서 수준 노드가 주어지면 이를 블록 요소의 배열로 쉽게 변환

  • Block 요소는 값

  • 참조를 염려할 필요없이 자유롭게 복사하거나 변경가능

  • 이것은 노드 조작에 매우 강력

  • 정의에 따라 값이 어떻게 만들어 졌는지 상관하지 않기 때문에 CommonMark 라이브러리를 전혀 사용하지 않고 코드에서 Markdown 구문 트리를 처음부터 만듦

  • 유형도 훨씬 명확

  • 컴파일러는 허용하지 않으므로 실수로 목록의 제목에 액세스하는 것과 같은 일을 할 수 없음

  • 코드를 안전하게 유지하는 것 외에도 매우 강력한 형식의 문서화

  • 유형을 살펴봄으로써 CommonMark 문서가 어떻게 구성되어 있는지 분명하게 알 수 있음

  • 주석과 달리 컴파일러는이 형식의 문서가 절대로 구식이 아닌지 확인

  • 이제 새로운 데이터 유형에서 작동하는 간단한 함수를 작성하는 것이 매우 쉬움

  • 예를 들어 Markdown 문서에서 목차에 대한 모든 레벨 1 및 2 헤더의 목록을 작성하려는 경우 모든 하위 항목을 반복하여 해당 항목이 헤더이고 올바른 수준인지 확인

func tableOfContents(document: String) -> [Block] {
  let blocks = Node(markdown: document)?.children.map(Block.init) ?? []
  return blocks.filter {
    switch $0 {
    case .heading(_, let level) where level < 3: return true
    default: return false
    }
  }
}
  • 이와 같이 더 많은 작업을 만들기 전에 블록을 노드로 다시 변환하는 역변환을 시도

  • 우리가 작성하거나 조작한 Markdown 구문 트리에서 CommonMark 라이브러리를 사용하여 HTML 또는 기타 텍스트 형식을 생성하고 라이브러리가 cmark_node_type만 처리 할 수 있기 때문에 필요

  • 우리는 Inline 값을 노드로 변환하는 노드와 Block 요소를 처리하는 노드라는 두 개의 초기화를 노드에 추가

  • 우리는 새로운 cmark_node를 처음부터 생성하는 새로운 초기화기로 Node를 확장함으로써 시작

  • 트리의 루트 노드 (그리고 재귀적으로 모든 자식)를 해제하는 deinit를 썼다는 것

  • 이것은 우리가 여기서 할당 한 노드가 결국 해제된다는 것을 보증

extension Node {
  convenience init(type: cmark_node_type, children: [Node] = []) {
    self.init(node: cmark_node_new(type))
    for child in children {
      cmark_node_append_child(node, child.node)
    }
  }
}
  • 텍스트 전용 노드 또는 여러 개의 자식 노드를 자주 만들어야 하므로 더 쉽게 사용하기 위해 세 가지 편의 초기화 추가
extension Node {
  convenience init(type: cmark_node_type, literal: String) {
    self.init(type: type)
    self.literal = literal
  }
  convenience init(type: cmark_node_type, blocks: [Block]) {
    self.init(type: type, children: blocks.map(Node.init))
  }
  convenience init(type: cmark_node_type, elements: [Inline]) {
    self.init(type: type, children: elements.map(Node.init))
  }
}
  • 이제 두 개의 변환 초기화를 작성할 준비
  • 방금 정의한 초기화를 사용하면 매우 간단
  • 요소를 켜고 올바른 유형의 노드를 만들고 인라인 요소의 버전은 다음과 같음
extension Node {
  convenience init(element: Inline) {
    switch element {
    case .text(let text):
      self.init(type: CMARK_NODE_TEXT, literal: text)
    case .emphasis(let children):
      self.init(type: CMARK_NODE_EMPH, elements: children)
    case .code(let text):
      self.init(type: CMARK_NODE_CODE, literal: text)
    case .strong(let children):
      self.init(type: CMARK_NODE_STRONG, elements: children)
    case .html(let text):
      self.init(type: CMARK_NODE_HTML_INLINE, literal: text)
    case .custom(let literal):
      self.init(type: CMARK_NODE_CUSTOM_INLINE, literal: literal)
    case let .link(children, title, url):
      self.init(type: CMARK_NODE_LINK, elements: children)
      self.title = title
      self.urlString = url
    case let .image(children, title, url):
      self.init(type: CMARK_NODE_IMAGE, elements: children)
      self.title = title
      urlString = url
    case .softBreak:
      self.init(type: CMARK_NODE_SOFTBREAK)
    case .lineBreak:
      self.init(type: CMARK_NODE_LINEBREAK)
    }
  }
}
  • 블록 레벨 요소에서 노드를 작성하는 것은 매우 유사
  • 약간 더 복잡한 경우는 목록뿐
  • 위의 노드에서 블록으로의 변환에서 CommonMark 라이브러리가 목록을 나타내는 데 사용하는 추가 노드를 제거했으므로 여기서 다시 추가
extension Node {
  convenience init(block: Block) {
    switch block {
    case .paragraph(let children):
      self.init(type: CMARK_NODE_PARAGRAPH, elements: children)
    case let .list(items, type):
      let listItems = items.map { Node(type: CMARK_NODE_ITEM, blocks: $0) }
      self.init(type: CMARK_NODE_LIST, children: listItems)
      listType = type == .Unordered
        ? CMARK_BULLET_LIST
        : CMARK_ORDERED_LIST
    case .blockQuote(let items):
      self.init(type: CMARK_NODE_BLOCK_QUOTE, blocks: items)
    case let .codeBlock(text, language):
      self.init(type: CMARK_NODE_CODE_BLOCK, literal: text)
      fenceInfo = language
    case .html(let text):
      self.init(type: CMARK_NODE_HTML_BLOCK, literal: text)
    case .custom(let literal):
      self.init(type: CMARK_NODE_CUSTOM_BLOCK, literal: literal)
    case let .heading(text, level):
      self.init(type: CMARK_NODE_HEADING, elements: text)
      headerLevel = level
    case .thematicBreak:
      self.init(type: CMARK_NODE_THEMATIC_BREAK)
    }
  }
}
  • 마지막으로, 사용자에게 멋진 인터페이스를 제공하기 위해 블록 레벨 요소의 배열을 가져와 문서 노드를 생성하는 public initializer를 정의
  • 그런 다음 문서 노드를 다른 출력 형식 중 하나로 렌더링
extension Node {
  public convenience init(blocks: [Block]) {
    self.init(type: CMARK_NODE_DOCUMENT, blocks: blocks)
  }
}
  • 이제 우리는 두 방향 : 문서를 로드하고 [Block] 요소로 변환하고, 요소를 수정한 다음 노드로 되돌림
  • 이를 통해 Markdown에서 정보를 추출하거나 Markdown을 동적으로 변경하는 프로그램을 작성
  • 먼저 C 라이브러리 (Node 클래스)에 얇은 래퍼를 작성하여 기본 C API에서 변환을 추상화
  • 이를 통해 우리는 관용적 인 Swift와 같은 인터페이스를 제공하는 데 집중
  • 전체 프로젝트는 GitHub-objio-commonmark-swift

An Overview of Low-Level Types

  • 표준 라이브러리에는 메모리에 낮은 수준의 액세스를 제공하는 많은 유형

  • 그들의 수는 압도적일 수 있지만 일관되게 이름 지어졌고 가장 중요한 명명 부품은 다음과 같음

    • managed 유형은 자동 메모리 관리 기능, 컴파일러는 메모리를 할당, 초기화 및 해제
    • unsafe 유형은 자동화된 메모리 관리(관리되는 것과 반대)를 제공하지 않음, 메모리를 명시적으로 할당, 초기화, 할당 해제 및 초기화 해제
    • buffer 유형은 단일 요소가 아닌 여러 개의 (연속적으로 저장된) 요소에서 작동
    • pointer 유형은 포인터 의미 (C 포인터 처럼)를 가짐
    • raw 유형은 유형이 지정되지 않은 데이터를 포함, 그것들은 C 에있는 void *와 같음, 이름에 raw를 포함하지 않는 타입은 데이터 타입을 가짐
    • mutable 유형은 포인터가 가리키는 메모리의 변형을 허용
  • 원시 저장소를 원하지만 C와 상호 작용할 필요가없는 경우 ManagedBuffer 클래스를 사용하여 메모리를 할당

  • 이것은 Swift의 컬렉션이 메모리를 관리하기 위해 사용하는 것

  • 이것은 요소의 수와 같은 데이터를 저장하기 위한 단일 헤더 값과 요소에 대한 인접한 메모리로 구성됨

  • 또한 실제 요소의 수와 같지 않은 capacity 속성을 가짐

  • 예를 들어 개수가 17 인 Array는 용량이 32 인 버퍼를 소유 할 수 있음

  • 즉, 배열 전에 15 개의 요소를 추가 할 수 있고 더 많은 메모리를 할당

  • ManagedBufferPointer라는 변종도 있지만 여기서는 다루지 않음

  • 때로는 수동 메모리 관리가 필요

  • 예를 들어 C API에서 객체를 함수의 컨텍스트로 전달

  • 그러나 C는 Swift의 메모리 관리에 대해 알지 못함

  • API가 동기식이면 Swift 객체를 전달하고 다시 변환할 수 있으며 모든 것이 좋을 것 (자세한 내용은 다음 섹션에서)

  • 그러나 API가 비동기이면 해당 객체를 수동으로 유지하고 해제

  • 그렇지 않으면 범위를 벗어나면 초기화되지 않을 수 있기 때문

  • 그렇게 하기 위해 Unmanaged 유형

  • 여기에는 유지 및 해제 메소드와 현재 보유 수를 수정하거나 유지하는 초기화 메소드가 있음

  • UnsafePointer는 포인터 유형 중 가장 단순하며 C의 const 포인터와 유사

  • 포인터가 가리키는 메모리의 데이터 유형에 대해 일반적

  • 이니셜라이저를 사용하여 다른 포인터 유형 중 하나에서 안전하지 않은 포인터를 만들 수 있음

  • Swift는 안전하지 않은 포인터를 사용하는 함수를 호출하는 특수 구문도 지원

  • 접두어 앞에 앰퍼샌드를 추가하여 올바른 형식의 변경 가능한 변수를 이러한 함수에 전달할 수 있으므로 in-out식

var x = 5
func fetch(p: UnsafePointer<Int>) -> Int {
  return p.pointee
}
fetch(p: &x) // 5
  • 함수 챕터에서 다루었던 inout 매개 변수와 똑같은 모양이며 비슷한 방식으로 작동

  • 이 경우 포인터가 변경 가능하지 않으므로 이 값을 통해 호출자에게 전달되는 것은 없음

  • Swift가 장면 뒤에서 생성하고 함수에 전달하는 포인터는 함수 호출 기간 동안만 유효

  • 함수에서 포인터를 반환하지 말고 함수가 반환 된 후에 액세스

  • 결과는 정의되지 않음

  • 앞서 말했듯이 Unsafe 형식은 메모리 관리를 제공하지 않으므로 fetch를 호출 할 때 x가 여전히 유효하다는 사실에 의존

  • UnsafeMutablePointer라는 이름의 변종

  • 이 구조체는 일반 C 포인터처럼 작동

  • 포인터를 역 참조 (dereference)하고 메모리 값을 변경하면 in-out 표현식을 통해 호출자에게 다시 전달

func increment(p: UnsafeMutablePointer<Int>) {
  p.pointee += 1
}
var y = 0 increment(p: &y)
y // 1
  • in-out 식을 사용하는 대신 UnsafeMutablePointer를 사용하여 직접 메모리를 할당
  • Swift에서 메모리를 할당하는 규칙은 C의 규칙과 비슷
  • 메모리를 할당 한 후에는 먼저 메모리를 초기화해야 사용할 수 있고 포인터를 다 끝내면 메모리 할당을 해제
// Allocate and initialize memory for two Ints
let z = UnsafeMutablePointer<Int>.allocate(capacity: 2)
z.initialize(to: 42, count: 2)
z.pointee // 42
// Pointer arithmetic:
(z+1).pointee = 43
// Subscripts:
z[1] // 43
z.deallocate(capacity: 2)
// Garbage memory
z.pointee // 42
  • C API에서 특정 요소 유형 (void * 또는 const void *)이 없는 바이트 시퀀스에 대한 포인터를 갖는 것도 매우 일반적

  • Swift의 동등한 기능은 UnsafeMutableRawPointer 및 UnsafeRawPointer 유형

  • void * 또는 const void *를 사용하는 C API는 이러한 유형으로 가져옴

  • 일반적으로 이러한 유형은 해당 인스턴스 메서드 중 하나 (예 : assumingMemoryBound(to:), bindMemory(to:) 또는 load(fromByteOffset: as:))를 사용하여 안전하지 않은 [변경 가능] 포인터 또는 다른 형식화 된 변형으로 변환

  • 때로는 C API에 불투명 포인터 유형

  • 예를 들어, cmark 라이브러리에서 cmark_node * 유형이 OpaquePointer로 가져 오는 것을 봤고

  • cmark_node의 정의는 헤더에 나타나지 않으므로 우리는 pointee의 메모리에 액세스 할 수 없음

  • 초기화를 사용하여 불투명 포인터를 다른 포인터로 변환

  • Swift에서 일반적으로 배열 유형을 사용하여 일련의 값을 연속적으로 저장

  • C에서는 시퀀스가 첫 번째 요소와 여러 요소에 대한 포인터로 반환되는 경우가 많음

  • 이러한 시퀀스를 컬렉션으로 사용하려면 시퀀스를 Array로 바꿀 수 있지만 요소의 복사본을 만듦

  • 이것은 종종 좋은 일 (일단 배열에 있으면 요소는 Swift 런타임에 의해 메모리 관리되기 때문)

  • 그러나 때로는 각 요소의 복사본을 만들고 싶지 않음

  • 이러한 경우 Unsafe[Mutable]BufferPointer 유형

  • 시작 요소와 카운트에 대한 포인터로 초기화하고 그때부터, (변경 가능한) 랜덤 액세스 콜렉션이 있음

  • 버퍼 포인터를 사용하면 C 컬렉션을 사용하는 것이 훨씬 쉬워짐

  • 마지막으로, Swift의 차기 버전에서는 Unsafe[Mutable]RawBufferPointer 유형이 추가 될 것

  • 따라서 원시 메모리를 콜렉션으로 사용하기가 더 쉬움(Data 및 NSData에 해당하는 저수준을 제공)

Function Pointers

  • 표준 C qsort 정렬 함수를 래핑하는 것을 보면 Swift의 Darwin 모듈에서 가져온 유형 (또는 Linux, Glibc의 경우)은 다음과 같음
public func qsort(
  _ __base: UnsafeMutableRawPointer!,
  _ __nel: Int,
  _ __width: Int,
  _ __compar: @escaping @convention(c) (UnsafeRawPointer?,
  UnsafeRawPointer?)
-> Int32)
  • 설명서 페이지 (man qsort)는 qsort 함수를 사용하는 방법을 설명

    • qsort() 및 heapsort() 함수는 nel 객체의 배열을 정렬
    • nel 객체의 초기 멤버는 base로 지정됨
    • 각 개체의 크기는 너비로 지정됨
    • 배열 베이스의 내용은 compare가 가리키는 비교 함수에 따라 오름차순으로 정렬됨
    • 비교되는 객체를 가리키는 두 개의 인수가 필요
  • 다음은 qsort를 사용하여 Swift 문자열 배열을 정렬하는 래퍼 함수

func qsortStrings(array: inout [String]) {
  qsort(&array, array.count, MemoryLayout<String>.stride) { a, b in
    let l = a!.assumingMemoryBound(to: String.self).pointee
    let r = b!.assumingMemoryBound(to: String.self).pointee
    if r > l { return -1 }
    else if r == l { return 0 }
    else { return 1 }
  }
}
  • qsort에 전달되는 각 인수를 살펴 보자

    • 첫 번째 인수는 배열의 밑수를 가리키는 포인터

    • Swift 배열은 UnsafePointer를 사용하는 함수로 전달할 때 C 스타일 기본 포인터로 자동 변환

    • 우리는 UnsafeMutableRawPointer (C 선언의 void *)이기 때문에 & 접두사를 사용

    • 함수가 입력을 변경시킬 필요가 없으며 C에서 const void * base로 선언 된 경우 앰퍼샌드가 필요하지 않음

    • 이것은 Swift 함수의 inout 인수와의 차이를 일치시킴

    • 둘째, 요소 수를 제공해야 함 이것은 쉬운데 우리는 배열의 count 속성을 사용

    • 세 번째, 각 요소의 너비를 가져 오려면 MemoryLayout.size가 아니라 MemoryLayout.stride를 사용

    • Swift에서 MemoryLayout.size는 유형의 실제 크기를 반환하지만 메모리의 요소를 찾을 때 플랫폼 맞춤 규칙은 인접한 요소 사이에 간격을 초래

    • stride은 유형의 크기와이 틈을 설명하기위한 약간의 여백 (0 일 수도 있음)

    • 문자열의 경우 크기와 stride이 Apple 플랫폼에서 동일하지만 모든 유형에 해당하지는 않음

    • 예를 들어 MemoryLayout.size는 9이고 MemoryLayout.stride는 16

    • 코드를 변환 할 때 C에서 Swift로, 아마도 sizeof를 C로 쓴다면 MemoryLayout.stride를 쓰고 싶을 것

    • 마지막 매개 변수는 배열에서 두 요소를 비교하는 데 사용되는 C 함수에 대한 포인터

    • Swift는 Swift 함수 유형을 C 함수 포인터에 자동으로 연결하므로 일치하는 서명이있는 함수를 전달

    • 그러나 하나의 큰 경고가 있습니다 : C 함수 포인터는 포인터, 그들은 어떤 값도 포착 할 수 없음

    • 이러한 이유 때문에 컴파일러는 외부 상태를 캡처하지 않는 함수 (예 : 로컬 변수가없고 제네릭이 없는 함수)만 제공

    • @convention(c) 속성으로 이것을 나타냄

  • compare 함수는 두 개의 원시 포인터를 허용

  • 이러한 UnsafeRawPointer는 모든 것에 대한 포인터가 될 수 있음

  • UnsafeRawPointer (UnsafePointer 이 아님)를 처리해야하는 이유는 C에 제네릭이 없기 때문

  • 그러나 우리는 String으로 전달된다는 것을 알고 있기 때문에 이를 String에 대한 포인터로 해석

  • 우리는 또한 포인터가 결코 여기에 없다는 것을 알기 때문에 안전하게 강제로 언 랩핑

  • 마지막으로 함수는 Int32를 반환, 첫 번째 요소가 두 번째 요소보다 크면 양수, 같으면 0, 첫 번째 요소가 두 번째 요소보다 작은 경우 음수가 반환

  • 다른 유형의 요소에 대해 작동하는 다른 래퍼를 만드는 것은 쉬움

  • 우리는 코드를 복사하여 붙여 넣을 수 있고 문자열을 다른 유형으로 바꿀 수 있음

  • 그러나 우리는 코드를 일반적인 것으로 만드는데 이것은 우리가 C 함수 포인터의 한계에 도달 한 곳

  • 이 책을 쓸 당시 Swift 컴파일러는 아래 코드를 segfault

  • 그렇지 않은 경우에도 코드는 여전히 불가능

  • 즉, 코드를 닫으면 외부에서 사물을 캡처

  • 보다 구체적으로는 각 일반 매개 변수에 대해 서로 다른 비교 연산자와 같음 연산자를 캡처

  • 우리가 이것에 관해서 할 수 있는 것은 아무것도 없음 - 우리는 단순히 C가 작동하는 방식의 한계에 봉착

// Valid syntax, but invalid code
extension Array where Element: Comparable {
  mutating func quicksort() {
    qsort(&self, self.count, MemoryLayout<Element>.stride) { a, b in
      let l = a!.assumingMemoryBound(to: Element.self).pointee
      let r = b!.assumingMemoryBound(to: Element.self).pointee
      if r > l { return -1 }
      else if r == l { return 0 }
      else { return 1 }
    }
  }
}
  • 이 제한에 대해 생각하는 한 가지 방법은 컴파일러와 같은 방법으로 생각하는 것

  • C 함수 포인터는 코드 블록을 가리키는 메모리의 주소

  • 컨텍스트가 없는 함수의 경우이 주소는 정적이며 컴파일 타임에 알려짐

  • 그러나 일반 함수의 경우 추가 매개 변수 (제네릭 형식)가 전달되므로 주소가 없음

  • 특수 제네릭 기능 이것은 폐쇄와 동일

  • 컴파일러가 함수 포인터로 전달할 수 있는 방식으로 클로저를 다시 작성할 수 있더라도 메모리 관리를 자동으로 수행 할 수 없음

  • 클로저를 언제 풀어야하는지 알 수있는 방법이 없음

  • 실제로 이것은 많은 C 프로그래머들에게도 문제

  • OS X에는 qsort_b라고하는 qsort 변형이 있는데, 마지막 매개 변수로 함수 포인터 대신 블록을 사용

  • 위의 코드에서 qsort를 qsort_b로 대체하면 컴파일되고 정상적으로 실행

  • 그러나 qsort_b는 대부분의 플랫폼에서 사용할 수 없음

  • 그리고 qsort 이외의 다른 함수는 블록 기반 변형을 가지고 있지 않을 수도 있고

  • 콜백과 함께 작동하는 대부분의 C API는 다른 솔루션을 제공

  • 추가 UnsafeRawPointer를 매개 변수로 사용하여 해당 포인터를 콜백 함수에 전달

  • 그런 다음 API 사용자는이 함수를 사용하여 콜백 함수를 호출 할 때마다 임의의 데이터 조각을 전달

  • qsort는 또한 정확히 이것을 수행하는 변형체 qsort_r을 가지고

  • 그 타입 시그니처는 UnsafeRawPointer 인 여분의 매개 변수 인 thunk를 포함

  • qsort_r은 모든 호출에서 이 함수에 값을 전달하기 때문에이 매개 변수가 비교 함수 포인터의 유형에도 추가됨

public func qsort_r(
  _ __base: UnsafeMutableRawPointer!,
  _ __nel: Int,
  _ __width: Int,
  _: UnsafeMutableRawPointer!,
  _ __compar: @escaping @convention(c)
  (UnsafeMutableRawPointer?, UnsafeRawPointer?, UnsafeRawPointer?)
  -> Int32
)
  • qsort_b를 우리 플랫폼에서 사용할 수 없다면 qsort_r을 사용하여 Swift에서 재구성
  • 우리는 UnsafeRawPointer로 캐스팅하는 한, 썽크 매개 변수로 원하는 것을 전달
  • 우리의 경우, 우리는 비교 종결을 전달
  • 우리는 in-out 식을 사용하여 var로 정의 된 변수에서 UnsafeRawPointer를 자동으로 생성
  • 그래서 우리가 해야 할 일은 인수로 전달 된 비교 클로저를 우리의 qsort_b 변형에 thunk라는 변수에 저장
  • 그런 다음 thunk 변수에 대한 참조를 qsort_r에 전달
  • 콜백 내부에서 void 포인터를 실제 유형 인 Block으로 캐스트 한 다음 단순히 클로저를 호출
typealias Block = (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32
func qsort_block(_ array: UnsafeMutableRawPointer, _ count: Int,
                _ width: Int, f: @escaping Block)
{
  var thunk = f
  qsort_r(array, count, width, &thunk) { (ctx, p1, p2) -> Int32 in
    let comp = ctx!.assumingMemoryBound(to: Block.self).pointee
    return comp(p1, p2)
  }
}
  • qsort_block을 사용하여 qsortWrapper 함수를 다시 정의하고 좋은 제네릭 인터페이스를 C 표준 라이브러리에있는 qsort 알고리즘에 추가
extension Array where Element: Comparable {
  mutating func quicksort() {
    qsort_block(&self, self.count, MemoryLayout<Element>.stride) { a, b in
      let l = a!.assumingMemoryBound(to: Element.self).pointee
      let r = b!.assumingMemoryBound(to: Element.self).pointee
      if r > l { return -1 }
      else if r == l { return 0 }
      else { return 1 }
    }
  }
}
var x = [3,1,2]
x.quicksort()
x // [1, 2, 3]
  • C 표준 라이브러리에서 정렬 알고리즘을 사용하는 것은 많은 작업처럼 보일 수 있지만 결국 Swift의 내장 정렬 기능은 사용하기가 훨씬 쉬우 며 대부분의 경우 더 빠름
  • 그러나 동일한 기술을 사용하여 유형이 안전한 일반 인터페이스로 래핑할 수 있는 많은 다른 흥미로운 C API가 존재
Clone this wiki locally