ExtendingtheSqueakVirtualMachine
- 스퀵 가상 머신 확장하기
원본-영어
http://stephane.ducasse.free.fr/FreeBooks/CollectiveNBlueBook/greenberg.pdf
번역진행
이화영 (Hwa Young Lee)
검수진행
없음
스퀵 가상 머신 확장하기
- Andrew C. Greenberg
- NetWolves Technology Corporation, Inc.
스퀵을 확장하는 이유는?
스퀵의 가상 머신의 확장은 다음의 중요한 질문에 대한 해답을 추측한다: 그것을 하는 이유가 무엇인가? 스퀵은 프로그래머가 하고자 하는 대부분의 일을 수행할 수 있는 매우 정교하고 이식 가능성이 매우 높은 프로그래밍 환경이다. 스퀵은 견고한 메모리 관리 및 디버깅 툴을 갖고 있어 시스템을 재앙적인 오류로부터 보호하고 프로그래머에게 오류의 원인을 알려준다. 스퀵은 스몰토크를 지원하기 때문에 객체 지향 프로그래밍 패러다임 또한 지원하여 재사용 가능성이 높고 확장 가능성이 큰 코드를 생산한다. 스퀵 프로그램은 다양한 플랫폼에 걸쳐 복잡한 애플리케이션을 수행하는 놀라운 성능을 보인다ㅡ동일한 픽셀별로(pixel-for-pixel).
반면 각 가상 머신("VM") 확장은 스퀵 객체 메모리 모델을 건너뛴다. 따라서 확장은 메모리 누수, 매달린 포인터(hanging pointer), 어드레스 오류, 전형적인 프로그래밍에서 잘못될 수 있는 여러 요인들로 인해 하드웨어의 충돌 가능성이 다시 생긴다. VM 확장은 종종 객체를 작동시키는 객체의 내부 구조에 관한 고정된 추측에 의존하며, 그에 따라 확장에 의존하는 객체의 재사용 가능성 범위나 객체의 확장 가능성을 제한한다. 아니면 스몰토크 객체 상의 루틴 동작이 너무 확장되면 이상하고 예측하지 못한 행위를 야기하기도 한다. 마지막으로 VM 확장은 머신 의존성을 야기하는 경향이 있다.
하지만 이러한 단점에도 불구하고 때때로 스퀵 VM을 확장시켜야 하는 강력한 이유들이 존재한다. 그 중 무엇보다 중요한 목표는 성능을 개선하고, 확장이 아니면 불가능한 기능 향상을 성취하는 데에 있다.
속도 높이기
스퀵 프로그램은 네이티브 기계어로 컴파일되지 않고, 대신 바이트코드라고 불리는 중간 표현으로 컴파일된다. 바이트코드는 스퀵 가상 머신이라는 컴퓨터 프로그램에 의해 해석되는데, 이 머신은 타겟 머신(target machine)에서 실행된다. 이러한 중간 처리 때문에 전체적인 시스템은 기계어로 직접 컴파일되는 C 언어 프로그램에 비해 일반적으로 더 느리게, 때로는 훨씬 느리게 실행된다. 또한 가상적으로 모든 프로그램의 운용에는 일부 머신 자원이 스몰토크 메시지의 전송과 메모리 관리 구문의 지원이 요구되기 때문에 스퀵 객체 모델은 어쩔 수 없이 오버헤드를 수반한다.
대부분의 경우 스퀵 설계와 관련된 결정에서 발생하는 이식성(portability), 전력(power), 안전에 대한 이점은 속도나 다른 자원에 드는 비용을 훨씬 능가한다. 게다가 점점 더 높아지는 속도의 프로세서 비용이 급락하면서 "금속 위에서" 속도의 필요성은 줄어드는 실정이다. 그럼에도 불구하고 속도가 무엇보다 중요한 경우도 있다ㅡ느린 속도가 기능의 부족으로 이어지는 경우.
현대 소프트웨어 설계 방법론들은 시스템 라이프 사이클에서 후기 단계가 될 때까지 성능 문제에 가진 관심을 늦추는 데에 중점을 둔다: "작동하게 만들고, 올바르게 만든 후 빠르게 만들어라." 작동이 가능한 동시에 믿을 만한 시스템의 모델이 존재하면 스몰토크 내에 완전히 포함된 전형적인 "조정(tuning)" 해결법들이 적절한 성능 해결책들을 제공하곤 한다. 프로그램은 그 프로그램 코드의 미비한 부분을 실행하는 데 대부분의 시간을 소비하는 경향이 있으므로, 이러한 접근법은 종종 효율적이고 뛰어난 결과를 도출하는 동시 프로그래머가 코드의 작은 부분만 조정하는 데 에너지를 소모하도록 해준다. 프로그램의 병목지점을 확인하기 위해 스퀵 프로파일러를 이용하면 때때로 특정 방법(들)이 확인되어 프로그램의 요구사항을 충족하기에 충분한 수준으로 향상되기도 한다.
이러한 접근법은 특히 스몰토크의 객체 지향 하부구조(infrastructure) 때문에 잘 작용한다. 주요 프로그램 로직을 변경하지 않고 단일 클래스의 내부 표현과 구현을 변경하는 것만으로도 완전한 프로그램이 엄청나게 향상되기도 한다. 병목현상이 그렇게 국소화되지 않은 곳에서는 속도 증가 노력을 촉진하기 위해 프로그램이 재팩토링되기도 한다. 그 외의 경우는, 프로그램이 사용하는 일반화된 시스템 클래스를 특수화시킴으로써 속도 증가를 맛볼 수 있다. 예를 들어, 스몰토크 Set 클래스를 사용하는 프로그램은 때때로 (메인 코드를 변경하지 않고) 기반이 되는 데이터의 특정 프로퍼티를 효율적으로 활용하는 다른 클래스를 대체함으로써 향상시킬 수도 있다.
하지만 때론 전형적인 조정으로 필수 기능에 충분한 속도 증가를 제공하지 못하기도 한다. 그러한 경우, 성능 병목지점을 직선으로 된(straight-line) 기계어로 대체하면 이전에 적절하게 전달되지 못했던 기능을 가능하게 만들지도 모른다.
생쥐처럼 활발하게: 새로운 힘과 기능
스퀵은 놀랍도록 대단하다. 표준 이미지는 임의의 정밀도 계산, 전반적인 컬렉션 프로토콜 집합, 여러 그래픽 사용자 인터페이스 프레임워크, 유연한 3D 그래픽 엔진, 재사용 가능한 웹 서버, 몇 가지 주요 내장된 "goodies"를 명명하기 위한 강력한 음향 처리 기능과 가장 전형적인 인터넷 프로토콜의 지원을 포함한다. 무엇보다 스퀵은 다수의 중요한 머신 함수로 저수준 접근성을 제공하는 primitive 의 광범위한 집합을 제공한다.
그럼에도 불구하고 컴퓨터의 기능적 성능은 계속 증가해왔기 때문에 스퀵의 설계자들은 시스템 수준의 primitive 에 대한 필요성을 모두 예상하지는 못했을 것이다. 때로는 저수준 드라이버나 로컬 운영체제 기능으로의 접근성에 대한 요구사항이 특정 함수를 실행하는 데 꼭 필요하다. 스퀵을 확장하는 능력이 없다면 일부 하드웨어는 전혀 재사용이 불가할지도 모른다.
다른 경우는, 기존의 머신 특정적이거나 이식 가능한 비스몰토크(non-Smalltalk) 라이브러리가 이미 존재하여 스퀵 애플리케이션에 유용하거나 필수적인 특정 성능들을 제공하기도 한다. 특히 라이브러리가 일반 유틸리티의 것이거나 다른 사람들에 의해 효과적으로 관리되는 경우, 스몰토크에서 wheel을 불가피하게 재개발하기 위해 자원을 전환하는 대신 스퀵의 확장을 코딩하여 해당 라이브러리의 기능으로 직접 접근을 제공하는 편이 바람직하다.
이 두 문제, 즉 저수준 드라이버로의 접근과 이미 존재하는 비스몰토크 라이브러리로의 접근 문제는 스퀵 확장을 이용하는 가장 공통된 이유에 속한다.
확장의 구성
스퀵은 다양한 방식으로 확장이 가능하다: interpreter 를 재작성하거나, 번호가 붙은 primitive (numbered primitive)를 추가하거나, 명명된 primitive 를 추가하는 방법이 있다. 앞의 두 방법은 전체 스퀵 시스템의 재빌드를 요한다. 현재까지 좀 더 유연하고 조정 가능한 해결책은ㅡ이번 장의 주제ㅡ명명된, 혹은 "플러그인 가능한(pluggable)" primitive 이다. 대부분의 시스템에서 이러한 명명된 primitive 는 Windows DLL 라이브러리와 같은 공유 라이브러리로서 구현 가능하다.
네이티브 운영체제 규칙을 이용하는 VM에 플러그인이 이용 가능해지면 (예: MacOS에서는 플러그인 파일을 특정 폴더로 드래그하면 이용 가능), 스몰토크 메서드는 primitive 에 대한 스몰토크 구문의 특수화된 확장을 이용해 그 primitive 중 하나를 호출할 수 있게 된다. 이 메서드와 (호출 메서드) 플러그인 내 프리미티크 함수는 특수화된 방식으로 통신한다. 결국 primitive 는 스몰토크 객체를 답(answer)하여 스몰토크로 제어(control)를 리턴한다.
명명된 primitive 로 스퀵을 확장시키는 과정에는 아래가 수반된다:
- 명명된 primitive 로 스몰토크 인터페이스를 생성하기
- 스몰토크 플러그인과 그 명명된 primitive 생성하기
플러그인 모듈
플러그인 자체는 기계어 루틴의 외부 라이브러리로, (i) 각각이 32-비트 정수 결과를 리턴하는 기본(parameterless; 매개변수 없는) 함수(원시함수)를 하나 또는 그 이상 포함하고, (ii) 단일 32-비트 매개변수를 취하고 setInterpreter 로 명명된 구분된 함수를 포함한다. 원시함수는 작성자에게 편리한 방식으로 생성 가능하지만 일반적으로 스몰토크의 하위집합, 즉 Slang이라 부르는 언어로 작성된 소스 코드로부터 생성되어 C로 번역된 후 기계어로 컴파일된다.
interpreter proxy
플러그인에서 어떤 primitive 든 그 첫 번째 호출 전에 스퀵 VM은 setInterpreter를 호출하여 interpreter proxy로 알려진 데이터 구조에 대한 포인터를 플러그인으로 전달한다. interpreter proxy는 VM의 다양한 내부 데이터 구조에 대한 포인터와, VM 내부 서브루틴의 하위집합에 대한 포인터를 포함한다. 플러그인은 스퀵과 통신 시 primitive 가 사용할 때를 대비해 공유 메모리에 interpreter proxy를 저장한다.
원시 함수에 대한 연결 규약
가동 중인 스퀵 VM은 모든 스몰토크 객체를 oop라 불리는 32-비트 정수 값으로 표현한다. primitive 를 호출하기 위해 VM은 실제 매개변수를 표현하는 oops와 primitive 메시지가 전송되는 객체를 표현하는 oop로 구성된 스택을 생성한다. primitive 는 이후 성공하거나 실패하는데, 성공 또는 실패의 여부는 successFlag 라 명명된 전역 VM 변수에 기록된다. primitive 가 성공하면 primitive 는 스택으로부터 매개변수와 전송자 oops를 꺼낼 것으로 기대하고, 그것이 빠진 장소에는 메시지에 대한 응답을 표현하는 단일 oop를 놓을 (또는 남겨둘) 것으로 예상된다. primitive 가 실패할 경우 primitive 는 스택을 변경하지 않은 채 떠날 것으로 기대된다. 원시 함수의 적절한 작동을 위해서는 이러한 연결 규약을 엄격하게 준수하는 것이 필요하다.
따라서 확장의 구성에 있어 핵심은 스퀵과 primitive 가 어떻게 서로 상호작용하는지를 이해하는지에 달려 있다. primitive 를 작성하려면 스퀵이 스몰토크 객체를 표현하는 방법, 스퀵 메모리 모델의 작동, VM과 primitive 간 정보의 표현이 공유되는 방식을 상세히 이해해야 한다.
본장의 나머지 부분에서는 플러그인 VM 확장을 빌드하는 기법을 논할 것이다. Slang Smalltalk 하위집합을 비롯해 스퀵 번역기와 interpreter 의 사용 방법을 간단히 소개하면서 시작할 것이다. 이후 스퀵이 어떻게 스몰토크 데이터 객체를 표현하는지와, 어떻게 interpreter proxy를 이용해 그 정보로 접근하고 조작하는지를 상세히 살펴보겠다. 다음으로는 원시 함수가 interpreter proxy를 이용해 스퀵 interpreter 와 상호작용하는 기법을 간단히 서술할 것이다. 마지막으로는 구조화된 큰 텍스트 블록을 조작하기 위해 플러그인 예제를 이용해 구체적으로 살펴봄으로써 내용을 마무리 짓고자 한다.
Slang으로 말하기
명명된 원시 함수는 적절한 연결 규약을 이용해 공유 라이브러리를 생성할 수 있는 어떤 언어로든 작성될 수 있지만 대부분은 Slang이라는 스몰토크의 하위집합에 해당하는 언어로 작성된다. Slang 코드는 스퀵 개발 환경에서 작성되고 검사된 후 플러그인으로 컴파일을 위해 C로 번역된다.
첫 플러그인
SmallInteger 인스턴스 17을 응답하는 단순한 플러그인 가능 primitive 를 빌드하는 간단한 예로 시작할 것이다. InterpreterPlugin 의 서브클래스를 생성함으로써 시작하고자 한다.
InterpreterPlugin subclass: #ExamplePlugin
InstanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'VMConstruction-Plugins'
Primitive 함수
InterpreterPlugin 은 interpreter proxy의 선언과 초기화를 위한 코드와 (interpreterProxy 로 명명된 인스턴스 변수에 저장될 것이다) 플러그인을 C로 번역하기 위한 기본구조를 포함해 플러그인을 구성하기 위한 기본 기능을 제공한다. 원시 함수를 생성하기 위해서는 ExamplePlugin 에 브라우저를 열고 다음 메서드를 추가하라.
answerSeventeen
"Extension to answer the SmallInteger seventeen"
self export: true.
interpreterProxy
pop: 1
thenPush: (interpreterProxy integerObjectOf: 17)
첫 번째 문은 스몰토크에서 해석 시 어떤 효과도 없지만 번역 중에는 번역된 프로시저를 결과 라이브러리에 있는 외부 라벨이 있는 프로시저로 식별한다. 두 번째 문은 스몰토크 컨텍스트 스택을 꺼낸 후 SmallInteger 인스턴스 17을 표현하는 객체 핸들(oop라 불리는)을 밀어낸다.
스몰토크 인터페이스
우선 코딩되고 나면 이 메서드는 워크스페이스에 아래를 실행함으로써 테스트할 수 있다:
ExamplePlugin doPrimitive: 'answerSeventeen'
"17"을 답으로 얻는다. 이 코드가 유용한 경우는 드물지만 primitive 가 어떻게 빌드되는지를 보여주는 코드다. 확장을 빌드하고 나면 스몰토크 인터페이스를 작성할 수 있다. primitive 는 아래와 같이 작성된 메서드에 의해 호출되는 것이 보통이다:
answerSeventeen
"Answer the SmallInteger seventeen"
<primitive: 'answerSeventeen' module: 'ExamplePlugin'>
^ExamplePlugin doPrimitive: 'answerSeventeen'
이 메서드를 실행하면 answerSeventeen primitive 를 호출하기 위해 노력하겠지만, primitive 를 이용할 수 없거나 primitive 가 이용 가능하나 실행 도중에 실패할 경우 나머지 스몰토크 코드를 해석하기 시작한다. 이 메서드가 Foo 클래스로 추가되었다고 가정하면 워크스페이스에 아래를 실행하여 primitive 를 테스트할 수도 있다:
Foo new answerSeventeen
플러그인 모듈은 아직 빌드하지 않았으므로 primitive 를 실패할 것이며, 해석된 코드의 결과를 응답할 것이다.
플러그인 해석하고 빌드하기
Slang 프로그램이 올바르게 작동하여 만족하면 아래를 실행하여 플러그인 라이브러리를 생성할 수 있다:
ExamplePlugin translate
이는 Slang 번역기를 호출하여 클래스 ExamplePlugin에 제시된 Slang에 상응하는 C 코드를 포함한 파일 "ExamplePlugin,c"를 생성하기 위해 실행한다.[1]
Slang: 간단한 개요
표현식
Slang은 정수, 문자, 기호, 문자열 등 그에 상응하는 C-언어 상수로 해석이 되는 스몰토크 리터럴을 인식한다. 기호는 문자열로 번역되고, 배열 리터럴은 허용되지 않는다. 표현식으로부터 식별자로의 대입(assignment)은 표현식의 번역으로부터 그에 상응하는 식별자로의 C 언어 대입으로 번역된다.
단항 메시지 전송
단항 메시지 전송은 주로 식별된 프로시저의 프로시저 호출로 해석되면서 수신자를 매개변수로서 전달한다. 따라서:
anObject frobosinate
위는 아래로 번역된다:
frobosinate(anObject);
이항 메시지 전송
이항 메시지 전송은 주로 식별된 프로시저의 프로시저 호출로 번역되면서 수신자를 첫 번째 매개변수로서 전달하고, 가장 우측의 인자를 두 번째 매개변수로서 전달한다. 따라서:
anObject frobosinateWith: aWidget
위는 아래로 번역된다:
frobosinateWith(anObject, aWidget);
키워드 메시지 전송
키워드 메시지 전송은 주로 식별된 프로시저의 프로시저 호출로 번역되면서 (콜론 없이 모든 키워드를 conacatenate함으로써) 수신자를 첫 번째 매개변수로서 전달하고 나머지 인자를 순서대로 전달한다. 따라서:
anObject frobosinateWith: aWidget andWith: anotherWidget
위는 아래로 번역된다:
frobosinateWithandWith(anObject, aWidget, anotherWidget);
Self 또는 InterpreterProxy로 메시지 전송
메시지가 "특수 객체"인 self나 interpreterProxy로 전송되면 위에 열거한 일반 규칙에서 예외가 발생한다. self 로 전송된 메시지는 위와 같이 번역되지만 첫 매개변수로서 self 가 없이 번역된다. interpreterProxy 로 전송된 메시지도 위와 같이 번역되지만, 함수 호출 앞에 "interpreterProxy->"라는 문자열에 붙어 있을 것이다. 따라서:
self frobosinateWith: a
위는 아래와 같이 번역된다:
frobosinateWith(a);
그리고
interpreterProxy integerObjectOf: 17
위는 아래와 같이 번역된다:
interpreterProxy->integerObjectOf(17);
내장(builtin)된 메시지 전송
특정 메시지는 앞의 규칙에 따라 번역되지 않고 그에 상응하는 스몰토크 연산과 비슷한 구문으로 된 C 언어 표현식으로 번역된다:
&, |, and:, or:, not, +, -, *, /, //, \\, <<, >>, min:, max:,
bitAnd:, bitOr:, bitXor:, bitShift:, bitInvert32, raisedTo:
<, <=, =, >, >=, ~=, ==,~~
기타 다른 메시지들도 아래와 같이 특별한 의미가 주어진다.
배열
아래 표현식은
foo at: exp
아래로 번역된다:
foo[exp]
그리고 아래 표현식은
foo at: exp1 put: exp2
아래로 번역된다:
foo[exp1] = exp2
basicAt: 와 basicAt:put: 메시지도 동일한 방식으로 번역된다.
제어 구조
아래 스몰토크 "제어 구조 메시지"는 비슷한 구문의 C 언어 문으로 번역된다:
[stmtList] whileTrue: [stmtList2]
[stmtList] whileFalse: [stmtList2]
[stmtList] whileTrue
[stmtList] whileFalse
exp1 to: exp2: do: [stmtList]
exp1 to: exp2: by: exp3 do: [stmtList]
exp1 ifTrue: [stmtList]
exp1 ifFalse: [stmtList]
exp1 ifTrue: [stmtList] ifFalse: [stmtList]
exp1 ifFalse: [stmtList] ifTrue: [stmtList]
stmt1. stmt2
사각 괄호는 이러한 제어 구조에서만 구문적 목적을 위해 사용되었음을 주목하라. Slang은 스몰토크 코드 블록을 지원하지 않는다.
메서드
스몰토크에서 메서드는 정수를 리턴하는 C 언어 함수로 번역될 것이다. 메서드가 키워드 형태로 선언될 경우 C 언어 함수는 모든 키워드를 연결(concatenate)함으로써 명명되지만 세미콜론은 사용되지 않을 것이다. 메서드는 위에서 설명한 바와 같이 스몰토크로부터 primitive 로서 직접 호출될 수도 있고, 아래 설명한 바와 같이 C 언어 코드로부터 서브루틴으로서 번역될 수도 있다. 임시 변수는 함수에 대한 지역 변수로서 번역될 것이다. 따라서:
frobosinateWith: a andWith: b
| temp |
...Slang code ...
위는 아래로 번역될 것이다:
int frobosinateWithandWith(int a, int b)
{
int temp;
... Translated C code ...
}
프로시저가 int가 아닌 다른 타입으로 된 값을 리턴하길 원할 경우 지시자(directive)를 이용할 수도 있다:
self returnTypeC: 'char *'
표현식의 강요(coercion)와 C 언어 선언
당신이 명시하지 않는 한 Slang은 모든 메서드 임시변수(method temporaries)를 정수 타입으로 정의된 C 변수로 번역할 것이다. Slang 지시어를 이용함으로써 변수를 번역된 코드에서 다르게 선언할 수 있다:
self var: #sPointer declareC: 'char *sPointer'.
C 타입 검사 구문을 충족시키기 위해서는 Slang 에게 표현식을 한 타입에서 다른 타입으로 강요하도록 지시할 필요가 있다. 이는 Slang 지시어를 이용하면 가능하다:
self cCoerce: aSmalltalkExpression to: 'int'
스몰토크 스택의 가장 위에 있는 객체가 String 클래스의 스몰토크 인스턴스와 같이 문자의 Array에 일치한다고 가정해보라. 그 요소로 접근하기 위해서는 아래 Slang 코드를 사용하길 원할 것이다:
self var: #stringObj declareC: 'int stringObj'.
self var: #stringPtr declareC: 'char *stringPtr'.
self var: #stringSize declareC: 'int stringSize'.
...
stringObj ←interpreterProxy stackValue: 0.
stringSize ←interpreterProxy stSizeOf: stringObj.
stringPtr ←self
cCoerce: (interpreterProxy arrayValueOf: stringObj)
to: 'char *'.
전역 변수
플러그인에 대한 전역 변수는 스몰토크에서 InterpreterPlugin 서브클래스의 인스턴스 변수로서 선언되고, 이후 InterpreterPlugin 서브클래스의 class 측면에 declareCVarsIn: 으로 명명된 메서드를 추가함으로써 C 언어 타입 정의의 목적에 맞게 선언할 수 있다. 하지만 전역적 선언을 이용하면 var: 키워드에 대한 매개변수로서 기호보다는 문자열이 사용된다. 예를 들어보겠다:
declareCVarsIn: cg
cg var: 'm23ResultX' declareC:'double m23ResultX'.
cg var: 'm23ResultY' declareC:'double m23ResultY'.
cg var: 'm23ArgX' declareC:'double m23ArgX'.
cg var: 'm23ArgY' declareC:'double m23ArgY'.
서브루틴
서브루틴은 대부분의 프로그래밍 상황에서 유용하다. 명명된 primitive 를 작성하는 것도 마찬가지다. 단순성을 위해 많은 명명된 원시 함수는 스몰토크와 작동하는(operative) 서브루틴 사이에서 "접착제"의 역할만 하면서 스택으로부터 매개변수를, 수신자로부터 데이터를 읽고 검증하는 일만 한다. 그 정보는 이후 작동하는 서브루틴으로 전달되고, 접착제 역할을 하는(glue) 원시 함수로 리턴되면 이 원시 함수는 정보를 삭제한 후 리턴값을 연결 규약에 따라 스택에 다시 밀어 넣는다.
Slang 서브루틴은 "특수 객체" self 로 메시지로서 전달함으로써 호출할 수 있다:
self subroutineOn: aFirstValue and: aSecondValue
그리고 서브루틴은 다음과 같이 코딩될 수 있을 것이다:
subroutineOn: aFirstParm and: aSecondParm
self var: #aFirstParm declareC: 'char *'.
self var: #aSecondParm declareC: 'float'.
self returnTypeC: 'float'.
... Slang code for the subroutine ...
이러한 내부 Slang 서브루틴에 대해서는 어떤 특수 연결 규약도 따를 필요가 없다. 그러한 연결 규약은 "<primitive: ...module:>" 메커니즘을 이용하는 스몰토크에서 직접 호출하는 프로시저에만 적용된다.
Slang에서 C로 번역하는(Slang-to-C) 번역기는 서브루틴이 충분히 짧거나, 코드 시작 또는 그 근처에서 Slang inline: 지시어 형태를 포함하는 경우, 자동으로 서브루틴을 inline시킨다:
self inline: true.
특정 서브루틴, 예를 들어 cCode: 지시어를 포함하는 서브루틴은 서브루틴이 inline: 지시어를 포함한다 하더라도 inline 되지 않을 것이다.
C 언어 코드 inline 하기
어떤 C 언어 표현식이든 아래 표현식을 이용하면 번역된 코드로 삽입될 수 있다:
self cCode: 'InternalMemorySystemSubroutine(foo)'
물론 이 코드는 코드가 해석 중일 때는 어떤 방식으로도 실행되지 않을 것이다. 하지만 C로 번역될 때 문자열은 축어(verbatim)에 삽입되고 뒤에 세미콜론이 붙을 것이다.
때때로, 특히 테스트 시에는 번역 도중에 특정 스몰토크 코드를 갖는 것이 유용하다. 그러한 이유로 표현식은
self cCode: 'InternalMemorySystemsubroutine(foo)'
inSmalltalk: [...Smalltalk code...]
위와 같이 번역될 것이지만 해석이 될 때는 그에 상응하는 스몰토크 코드가 실행될 것이다.
TestInterpreterPlugin
스퀵은 두 번째로 상향 호환이 가능한 Slang 하위집합 번역기인 TestCodeGenerator 를 제공한다. 머지 않아 TestCodeGenerator 는 현재 Slang 해석기를 대신할 것으로 예상된다. TestCodeGenerator는 위에서 설명한 번역기와 동일한 기능을 제공하지만 자동으로 생성되는 플러그인 연결 코드의 수단도 제공할뿐만 아니라 플러그인 가능한 primitive 의 작성을 크게 가능하게 한다.
TestCodeGenerator를 사용하기 위해서는 플러그인 클래스를 InterpreterPlugin 대신 TestInterpreterPlugin 의 서브클래스로서 정의하라. TestCodeGenerator에 대한 완전한 설명은 본 문서의 범위에서 벗어난다. 본 문서가 작성될 당시에는 아쉽게도 문서가 매우 부족했지만 곧 더 많은 자료들이 생길 것이다. 그때까지 아래의 사이트를 이용하면 도움이 될 것이다:
다음 절에서는 스퀵 가상 머신에서 표현되는 객체의 내부 구조를 더 깊게 논하고자 한다.
스몰토크 객체의 모양
스몰토크에서는 모든 것이 객체다. 하지만 결국 객체는 하드웨어에선 비트와 바이트로 표현되어야 한다. 스몰토크 모델의 추상화의 도움을 받지 않고는 primitive 와 스퀵 VM은 이 불편한 현실을 염두에 두어야 한다. primitive 를 작성하기 위해서는 스퀵 VM은 모든 스몰토크 객체를 oop로 알려진 32-비트 정수 값으로 표현한다는 사실을 프로그래머가 이해해야 한다. 하지만 이러한 oop의 내부 표현과 해석은 oop에 의해 표현되는 객체에 따라 상당히 달라진다. 스퀵 객체 표현의 네 가지 다른 범주, 또는 "모양"을 고려하고 이것이 Slang과 C에서 어떻게 조작되는지도 고려하면 도움이 될 것이다.
- 스몰토크 SmallInteger 객체;
- 다른 색인 불가(non-indexable) 스몰토크 객체;
- oop로 색인 가능한 스몰토크 객체;
- oop이외의 데이터로 색인 가능한 스몰토크 객체.
스몰토크의 관점에서 보면 첫 번째 범주는 자체 설명적이다. 두 번째 범주는 가령 Boolean, Fraction, Object 클래스의 인스턴스를 포함하지만, Form 또는 Set 클래스의 인스턴스처럼 스스로를 색인하진 않지만 인스턴스 변수로서 색인된 객체를 포함하는 객체들도 두 번째 범주에 포함된다. 세 번째 범주는 oops로 색인 가능한 객체로, Array 클래스의 인스턴스를 포함한다. 마지막, oop를 제외한 데이터로 색인 가능한 스몰토크 객체는 LargePositiveInteger, ByteArray, WordArray 클래스의 인스턴스처럼 바이트나 워드 데이터로 색인 가능한 객체를 포함한다.
SmallInteger 객체
스퀵은 주로 효율성을 이유로 정수 데이터를 포함하는 oop를 이용해 31-비트 부호가 있는 정수 값을 (스몰토크 SmalltalkInteger 객체) 표현한다. 다른 타입의 데이터를 표현하는 oop 는 메모리 내 다른 곳에 보관된 객체 "헤더"에 대한 포인터들이다. 모든 객체 헤더는 단어 경계(word boundary)에서 시작되므로, 이러한 포인터 oop 는 홀수이다. 스퀵은 이러한 사실을 이용해 홀수로 된 oop 에 SmallInteger 를 표현한다. SmallInteger Oop 는 따라서 아래의 포맷으로 보관된다:
Bit Index | 31 30 29 ...... 3 2 1 | 0 |
Data | 31-bit SmallInteger Data | 1 |
모든 SmallInteger 는 int-type C 변수로 표현이 가능하지만 그 반대는 불가능하다. 230 이상의 값 또는 -230보다 적은 값을 포함한 C int 변수는 SmallInteger 데이터에 이용 가능한 31 비트에 "맞지" 않을 것이다.
SmallInteger Oops와 값의 변환
interpreter proxy는 SmallInteger 의 oop 표현과 실제 표현되는 수치 값을 왔다갔다 할 수 있도록 도와주는 두 가지 메서드를 제공한다:
스몰토크 | C |
interpreterProxy integerObjectOf: value | interpreterProxy->integerObjectOf(value) |
interpreterProxy integerValueOf: oop | interpreterProxy->integerValueOf(oop) |
integerValueOf: 의 인자가 SmallInteger 객체를 표현하는 oop인 경우, 메서드는 SmallInteger 에 상응하는 부호가 있는 C 값을 응답할 것이다. 그렇지 않을 경우, 설사 인자가 LargePositiveInteger와 같은 정수 값을 표현한다 하더라도 이 답은 정의되지 않을 것이다. 반대로 integerObjectOf 로의 인자가 SmallInteger 에 의해 표현 가능한 값의 범위 내에 있는 경우, 메서드는 그에 상응하는 SmallInteger oop으로 응답할 것이다. SmallInteger가 값을 표현할 수 없을 경우 결과는 정의되지 않을 것이다.
SmallInteger Oops와 값 검사하기
isIntegerValue: 메서드는 인자를 SmallInteger 객체로 변환 가능한지 여부를 반영하는 boolean 값으로 응답한다. 즉, 값이 SmallInteger 범위 내에 있는지를 검사하는 것이다. 반대로 isIntegerObject: 는 oop으로 취급되는 인자가 SmallInteger 를 표현하는지 여부를 반영하는 boolean 값으로 응답한다.
interpreterProxy isIntegerObject: oop interpreterProxy->isIntegerObject(oop)
interpreterProxy isIntegerValue: value interpreterProxy->isIntegerValue(value)
변환 함수 검증하기
interpreter proxy는 하나의 호출로 SmallInteger 일 것으로 예상되는 값을 검증하고 로딩하는 메서드를 제공한다:
interpreterProxy interpreterProxy-> checkedIntegerValue: oop checkedIntegerValue (oop)
이 메서드는 oop 가 SmallInteger 값인지를 검사하고, 그 값이 맞다면 그에 상응하는 정수 값을 리턴한다. Oop 가 SmallInteger 값이 아닐 경우, successFlag가 false로 설정되고, 결과는 정의되지 않을 것이다.
SmallInteger 범위를 벗어난 긴 정수 값
때로는 extension pass를 갖거나 전체 32-비트 정수 값을 리턴하는 방법이 유용하다. interpreter proxy는 부호가 없는 32-bit 값을 조작하는 함수를 제공한다:
interpreterProxy interpreterProxy-> positive32BitIntegerFor: value positive32BitIntegerFor(value) interpreterProxy interpreterProxy-> positive32BitValueOf: oop positive32BitValueOf(oop)
positive32BitValueOf: 메서드는 SmallInteger 또는 4-바이트 LargePositiveInteger 객체를 수락하고, 부호가 없는 긴 정수로 저장 및 조작할 수 있는 값을 응답할 것이다. Positive32BitIntegerFor: 메서드는 값이 SmallInteger 범위에 있는 경우 unsigned long int C 변수에 보관된 값을 변환하고 그에 상응하는 SmallInteger 에 대한 oop을 리턴할 것이고, 범위를 벗어난다면 LargePositiveInteger에 대한 oop를 리턴할 것이다.[2]
기타 색인 불가 객체
기타 모든 oop은 VM에서 oop의 추가 설명을 포함한 내부 데이터 구조에 대한 포인터로서 표현된다. 스몰토크 클래스 대다수는 이러한 모양을 갖는다.
인스턴스 변수가 없는 객체
색인 불가 모양을 한 일부 객체는 그들이 멤버(member)로 있는 클래스 외의 다른 데이터는 수용하지 않는다. 명명된 primitive 가 그러한 객체에 대한 oop를 이용해 할 수 있는 유일한 의미 있는 일은 다른 pop 인스턴스 "슬롯"으로 값을 할당하거나, 이를 다른 oop와 비교하는 일 밖에 없다. 일부 중요한 특수 객체에 대한 oop는 해석기에 의해 제공된다:
스몰토크: | C: |
interpreterProxy falseObject | interpreterProxy->falseObject() |
interpreterProxy nilObject | interpreterProxy->nilObject() |
interpreterProxy trueObject | interpreterProxy->nilObject() |
스몰토크 true 에 대한 oop는 0이 아닐 가능성이 크기 때문에 C-언어의 if-문에서 true로 검사할 것임을 주목하는 것이 중요하다. 하지만 스몰토크 객체 false 를 표현하는 oop 도 마찬가지다. 따라서 C 에서 oop 가 boolean 의 true 를 표현하는지 결정하려면 아래와 같은 것을 코딩해야 한다:
if (booleanOop == interpreterProxy->trueObject()) {...}
이 방법 대신 아래와 같은 연산을 이용하면 스몰토크 Boolean 객체를 C-언어의 Boolean 값으로, 또 그 반대로도 변환한다:
interpreterProxy booleanValueOf: oop interpreterProxy->booleanValueOf(oop)
checkedIntegerValue: 와 같은 booleanValueOf: 메서드는 먼저 oop가 Boolean 타입의 객체를 표현하는지 검사하고, 표현하지 않는다고 판단되면 successFlag를 false로 설정할 것이다. Oop가 무효할 경우 메서드는 그에 상응하는 C-언어의 boolean 값으로 응답할 것이다.
스몰토크 nil에 대한 oops 는 true 와 false 에 대한 oops와 마찬가지로 C 언어 상수 NULL과 동일한 정수 값은 수용하지 않을 것이다. 아래 코드는:
if (oop == NULL) {...}
예상대로 행동하지 않을 것인데, oop가 사실상 스몰토크 객체 nil 을 표현할 것인지 여부와 상관없이 C 표현식의 결과가 거의 false가 될 것이기 때문이다. Oop가 nil을 표현하는지를 C에서 테스트하려면 아래와 같이 코딩해야 한다:
if (oop == interpreterProxy->nilObject()) {... }
명명된 인스턴스 변수가 있는 객체
하지만 스몰토크 객체의 대다수는 하나 또는 그 이상의 인스턴스 변수가 있는 색인 불가 객체를 정의한다. 이러한 인스턴스 변수는 0부터 시작되는 번호가 붙은 슬롯에 보관된다. 슬롯 번호는 그러한 이름이 Class 정의에 열거된 순서대로 따른다. interpreter proxy는 그러한 모양으로 된 객체에 상응하는 oop를 조작하기 위한 아래 함수를 제공한다:
스몰토크 | C |
interpreterProxy | interpreterProxy-> |
fetchWord: slot
ofObject: oop |
fetchWordofObject (slot,oop) |
interpreterProxy | interpreterProxy-> |
firstFixedField: oop | firstFixedField(oop) |
fetchWord:ofObject: 가 리턴한 값은 해당하는 인스턴스 변수에 보관된 객체에 상응하는 oop 이다. firstFixedField: 가 리턴한 값은 해당하는 인스턴스 변수를(String에서는 워드 기반의 CArrayAccessor) 리턴하기 위해 색인 가능한 워드 기반의 포인터이다. 따라서,
p ←interpreterProxy firstFixedField:oop.
0 to: numInstVars do: [:i | self foo: (p at: i)]
int *p;
p = interpreterProxy->firstFixedField(oop);
for (i=0; i<numInstVars; i++) {foo(p[i]);}
위는 oop가 표현한 객체에 각 변수에 해당하는 oop 상에서 함수 foo를 실행한다.
interpreter proxy는 객체의 인스턴스 변수 내용의 추출, 타입 검사, 그리고 부동 소수점과 정수의 경우에는 값의 변환 기능까지 동시에 제공한다.
interpreterProxy | interpreterProxy->fetchIntegerofObject( |
fetchInteger: slot
ofObject: oop |
slot,oop) |
interpreterProxy | interpreterProxy-> |
fetchPointer: slot
ofObject: oop: |
fetchPointerofObject (slot, oop) |
부가적 효과로는, oop 가 SmallInteger 를 따르지 않는 한 fetchInteger:ofObject: 는 실패할 것이며, oop 가 SmallInteger 를 따를 경우 fetchPointer:ofObject: 는 실패할 것이다.
예를 들어, rcvr 가 아래와 같이 정의된 클래스의 인스턴스에 대한 oop를 포함한다고 가정하자:
Object subclass: #Example
instanceVariableNames: 'instVar0 instVar1'
classVariableNames: ''
poolDictionaries: ''
category: 'Squeak-Plugins'.
그 때 instVar()와 instVar1가 ByteArray 객체와 SmallInteger 객체를 각각 포함한다면 당신은 다음과 같이 rcvr로부터 그러한 객체에 대한 oop 포인터를 로딩할 수 있다:
oop0 ←interpreterProxy fetchPointer: 0 ofObject: rcvr.
oop1 ←interpreterProxy fetchInteger: 1 ofObject: rcvr.
마지막으로, 유효한 스몰토크 값은 interpreter proxy 메서드를 이용해 객체의 슬롯에 보관할 수 있다:
interpreterProxy | interpreterProxy-> |
storeInteger: slot | storeIntegerofObjectwithValue |
ofObject: oop | (slot, oop, integerValue) |
withValue: integerValue | |
interpreterProxy storePointer: | interpreterProxy-> |
storePointer: slot | storePointerofObjectwithValue |
ofObject: oop | (slot, oop, nonIntegerOop) |
withValue: nonIntegerOop |
storeInteger:ofObject:withValue: 를 이용하면 integerValue 는 SmallInteger oop로 변환되고, 명시된 인스턴스 변수에 보관될 것이다 (변환할 수 없다면 실패할 것이다). 다른 메서드는 oop를 변환하지 않고 (하지만 nonIntegerOop 가 SmallInteger 를 위한 것이라면 실패할 것이다) 명시된 인스턴스 변수에 객체를 보관한다.
Oops로 색인 가능한 객체
명명된 인스턴스 변수 외에 객체는 다양한 수의 색인 가능한 객체를 포함할 수 있다. 물론 가장 주된 예로 스몰토크 클래스 Array가 있다. 색인 가능한 객체는 다른 스몰토크 객체를 참조하는 oop를 포함하거나, raw 수치 데이터를 바이트 또는 워드로 포함할 수 있다. 비색인가능 객체와 마찬가지로 스몰토크 객체로 색인 가능한 객체 또한 (하지만 데이터로 색인 가능한 객체는 해당하지 않는다!) 명명된 인스턴스 변수의 수를 원하는 만큼 가질 수 있다.
색인 가능한 인스턴스 변수 추출하기
Oop가 주어지면 아래 코드를 이용해 그에 상응하는 객체의 크기를 얻을 수 있다 (size 메시지를 전송 시 객체가 응답하게 될 값):
interpreterProxy stSizeOf: varOop | interpreterProxy->stSizeOf(varOop) |
혹은 객체의 변수 부분이 소요하는 바이트 수에만 관심이 있다면 아래를 이용해도 좋다:
interpreterProxy | interpreterProxy-> |
byteSizeOf: varOop | byteSizeOf(varOop). |
oop가 주어지면 아래를 이용해 그 요소의 oop 값을 얻거나 변경할 수도 있다:
interpreterProxy | interpreterProxy-> |
stObject: varOop at: index |
stObjectat(varOop, index) |
interpreterProxy | interpreterProxy -> |
stObject: varOop at: index put: value |
stObjectatput(varOop,index,value) |
아래를 이용하면 객체의 변수 부분에 보관된 첫 번째 색인 가능 oop로의 C 포인터를 얻을 수 있다:
p ←self | p = (int *) interpreterProxy-> |
cCoerce: (interpreterProxy firstIndexableField: oop) to: 'int *' |
firstIndexableField(oop) |
이후에 배열을 색인함으로써 각 oop를 강조(또는 변경)할 수 있다. 스몰토크든 C든 interpreter proxy를 통해 참조된 모든 변수가 색인된(variable indexed) 객체는 0부터 시작된다. firstIndexableField: 는 'void*' 타입의 객체를 응답하고, 결과는 'int*'로 강요된다.
명명된 인스턴스 변수 추출하기
Oop로 색인 가능한 객체는 색인 가능한 인스턴스 변수 외에도 명명된 인스턴스 변수를 가질 수 있다. 그러한 인스턴스 변수의 존재 유무를 검사하고, 색인 불가 인스턴스 변수와 마찬가지로 조작 가능하다.
Oops로 색인 가능한 객체 검사하기
다음을 이용하면 객체가 색인 가능한(변수인)지 검사할 수 있다:
interpreterProxy isIndexable: oop interpreterProxy->isIndexable(oop)
그러면 oop는 객체가 oop, 바이트 또는 워드 중 무엇으로 색인 가능한지 여부와 상관없이 어떤 변수 객체든 일치한다면 true를 리턴할 것이고, 아래를 이용하면:
isPointers: oop
oop가 oops로 색인 가능한 변수 객체에 일치할 경우에만 true를 리턴할 것이다.
예제
아래 코드는 oop가 표현하는 변수 스몰토크 객체에서 객체의 순서를 제자리에서 역전시킨다.
a ←self cCoerce: (interpreterProxy firstIndexableField: oop) to: 'int *'.
i ←0. j ←(interpreterProxy stSizeOf: oop) - 1.
[i<j] whileTrueDo:
[t ←a at: i. a at: i put: (a at:j). a at: j put: t. i ←i + 1. j ←j - 1].
아니면 C에서는 다음과 같이 된다:
a = (int *) interpreterProxy->firstIndexableField(oop);
i = 0; j = (interpreterProxy->stSizeOf(oop)) – 1;
while(i<j) {t = a[i]; a[i]=a[j]; a[j]=t; i++; j--;}
1-바이트 또는 4-바이트 값으로 색인 가능한 객체
스몰토크는 각각이 1 바이트 또는 4-바이트 워드를 포함하고 각자가 색인(indexing)에 의해 참조되는 바이너리 데이터를 포함하는 색인 가능한 인스턴스 변수가 있는 객체의 생성을 허용한다. 이러한 모양의 객체는 명명된 인스턴스 변수를 포함하지 않을 지도 모르므로, 명명된 인스턴스 변수를 다룬 절에서 설명한 메서드는 그러한 객체들에겐 적용되지 않는다.
스몰토크는 인스턴스 변수 데이터를 바이트 또는 워드의 배열로 내부에서 표현한다는 사실은 놀랍지도 않다. Oops로 색인 가능한 객체와 마찬가지로 변수 바이트나 변수 워드 객체의 배열에 대한 포인터는 fistindexableField: 를 이용해 얻을 수 있다. (변수 바이트 객체의 경우 결과를 'int*' 대신 'char*'로 강요해야 한다.) 마찬가지로 그러한 객체의 크기는 stSizeOf: 를 이용해 얻을 수 있다 (워드 색인 가능 객체의 경우, 메시지는 4-바이트 워드로 응답할 것이고, 바이트 색인 가능 객체의 경우 메서드는 oop가 표현하는 객체 내 바이트 수를 응답할 것임을 명심하라). 바이트 색인이 가능한지 혹은 워드 색인이 가능한지 여부와 상관없이 객체 내 바이트의 수를 원한다면 byteSizeOf: 를 대신 사용해도 좋다.
아래 함수를 이용해 oop의 모양을 확인할 수 있다:
interpreterProxy isBytes: oop interpreterProxy->isBytes(oop)
interpreterProxy isWords: oop interpreterProxy->isWords(oop)
interpreterProxy isWordsOrBytes: oop interpreterProxy->isWordsOrBytes(oop)
마지막으로 아래를 이용해 바이트 또는 워드 객체의 인증과 변환을 한 단계로 결합하여:
interpreterProxy | interpreterProxy-> |
fetchArray: index ofObject: oop |
fetchArrayofObject(index,oop) |
명명된 인스턴스 변수 배열로부터 oop 포인터를 추출하거나,
interpreterProxy arrayValueOf: oop interpreterProxy->arrayValueOf(oop)
위를 이용해 어떤 oop에서든 oop 포인터를 추출할 수 있다. 명시된 oop가 바이트 또는 워드 색인이 가능하지 않을 경우 두 메서드는 실패할 것이며, 나머지 경우는 포인터를 리턴할 것이다.
Float의 특수 사례
부동 소수점 값은 스몰토크에서 스칼라 값으로 취급되긴 하지만 두 개의 32-비트 워드로 구성된 색인 가능 객체의 형태에서 64 비트로서 표현된다. interpreter proxy는 Float 객체를 표현하는 oop을 double 타입의 C 값으로, 그리고 반대로도 변환하는데, 이 때 사용하는 메서드는 SmallInteger 에 사용하는 것과 동일하다.
interpreterProxy | interpreterProxy-> |
floatObjectOf: aFloat | floatObjectOf(aFloat) |
interpreterProxy | interpreterProxy->floatValueOf(oop) |
floatValueOf: oop | |
interpreterProxy | interpreterProxy-> |
fetchFloat: fieldIndex | fetchFloatofObject(fieldIndex, |
ofObject: objectPointer | objectPointer) |
interpreterProxy | |
isFloatObject: oop | interpreterProxy->isFloatObject(oop) |
명명된 primitive 의 구조
primitive 는 보통 수신자가 아래와 같이 정의된 메시지를 받을 때 스몰토크 표현식을 평가하는 과정에서 호출된다:
primitiveAccessorNameWith: object1 then: object2 andThen:
object3
<primitive: 'primitiveName' module: 'ExtensionPluginName'>
"...
Smalltalk Code to be executed if the primitive fails or cannot be loaded
..."
물론 메서드의 이름과 그 매개변수의 수와 이름은 다양할 수 있다. 메시지가 전송되면 수신자에 대한 oop는 스택 위로 밀려 들어가고, 각 매개변수가 평가된 후 메서드의 정의부에 나타나는 순서대로 스택에 밀려 들어간다. VM 전역 변수 successFlag 는 true로 설정된다. 이후 스택은 아래와 같은 모양을 한다:
(top) | 0 | oop for object3 |
1 | oop for object2 | |
2 | oop for object1 | |
(bottom) | 3 | oop for receiver |
모듈이 이미 로딩되지 않았다면 스퀵 VM은 명명된 플러그인, 이번 경우 ExtensionPluginName의 "로딩"을 시도할 것인데, 이는 운영체제에 따라 다른 방식으로 이루어질 것이다. 스퀵은 이후 setInterpreter 함수를 찾아 실행을 시도하면서 그 함수로 스퀵 VM interpreter proxy에 대한 포인터를 전달한다. (표준 플러그인 코드는 이 값을 interpreterProxy 로 명명된 전역 변수에 저장한다.) 이 과정이 성공하면 스퀵은 명명된 원시 함수, 이번 경우엔 primitiveName 에 대한 포인터를 찾아낼 것이다. 이 과정 중 일부라도 실패하면 vM은 확장의 로딩 시도를 중단하고, primitive 명세를 따른 스몰토크 코드를 실행하여 진행할 것이다.
하지만 모든 일이 순조롭게 진행되면 제어는 명명된 원시 함수로 전달된다. primitive 가 실패하여 successFlag가 false로 설정되면 primitive 는 리턴하기 전에 스택을 원래 상태로 (또는 복구하여) 남겨둬야 한다. primitive 가 실패하지 않을 경우 primitive 는 스몰토크 스택에서 매개변수와 수신자 oop을 꺼낸 후 리턴 값의 역할을 하도록 유효한 oop을 리턴 값을 밀어 넣어야 한다.
이는 플러그인을 작성하면서 가장 중요한 사항에 속하고, 예측할 수 없는 행위의 가장 공통된 원인에 속한다. 이러한 연결 규약을 준수하지 못할 경우 정의되지 않은 행위를 상당히 많이 야기하고 스퀵 VM을 freeze 시키거나 충돌하게 될 가능성이 크다.
primitive 가 실행에서 돌아오면 해석기는 successFlag를 검사할 것이다. primitive 가 실패하면 제어가 그에 해당하는 스몰토크 코드로 전달된다. 반대로 성공한다면 스몰토크 스택이 다시 (한 번만) 꺼내져 oop를 얻고, 이는 primitive 메시지 전송에 대한 응답의 역할을 할 것이다.
primitive 는 다음 절에서 설명할 함수를 이용해 interpreter proxy를 통해 스몰토크 스택을 (C 함수와 매개변수 스택과는 별개로) 조작한다.
Interpreter 스택으로 Primitive 접근
명명된 primitive 함수는 아래 함수를 이용해 스택을 조작할 수도 있다:
interpreterProxy stackValue: | offset interpreterProxy->stackValue(offset) |
interpreterProxy pop: nItems | interpreterProxy->pop(nItems) |
interpreterProxy push: oop | interpreterProxy->push(oop) |
interpreterProxy | interpreterProxy->popthenPush(nItems,oop) |
pop: nItems | |
thenPush: oop |
stackValue: offset 메서드는 스몰토크 스택 오프셋 슬롯의 값을 위에서부터 응답한다. 따라서,
oop ←interpreterProxy stackValue: 0.
스택의 최상에 있는 값을 리턴한다. pop: 메서드는 스몰토크 스택의 최상에서 상단의 nItems 요소를 제거하고, 제거된 마지막 값에 대한 oop를 응답한다. 반대로 push: 는 매개변수를 스몰토크 스택 위로 밀어 넣는다. pop:thenPush: 메서드는 nItems 를 제거한 후 명시된 oop를 스택 위로 밀어 넣는다.
명명된 primitive 는 그것이 실패할 것인지 성공할 것인지 처음부터 아는 경우가 드물기 때문에 primitive 가 스택에서 값을 꺼내고 실패 상태를 확인한 후 단순한 리턴을 통해 "황급히 후퇴(hastry retreat)"할 수 있는 장소에 값을 남겨두는 경우는 흔치 않다. 따라서 primitive 는 그 매개변수로 접근하기 위해 pop과 관련된 루틴보다는 stackValue: 를 사용할 가능성이 훨씬 크다. interpreter proxy는 명시된 위치에서 pop을 로딩할 뿐만 아니라 C와 익숙한 값, 매개변수, 수신자로 oop를 변환하고 그 모양을 검증하는 하나의 단계를 통해 이 과정을 촉진시키는 함수를 다양하게 제공한다. 그러한 함수는 다음과 같다:
interpreterProxy stackIntegerValue: offset | interpreterProxy->stackIntegerValue(offset) |
interpreterProxy stackObjectValue: offset | interpreterProxy->stackObjectValue(offset) |
interpreterProxy stackFloatValue: offset | interpreterProxy->stackFloatValue(offset) |
primitive 에 중요한 인스턴스 변수가 있는 객체나 변수 객체의 경우 oop는 stackValue: 를 이용해 로딩된 후 검증되거나 변환될 것이다. 마지막으로 프록시는 C에 익숙한 값을 oop로 쉽게 변환하고 한 단계만 통해 그에 상응하는 oop를 스택으로 밀어넣는 메커니즘을 다음과 같이 제공한다:
interpreterProxy pushBool: cValue | interpreterProxy->pushBool(cValue) |
interpreterProxy pushFloat: cDouble | interpreterProxy->pushFloat(cDouble) |
interpreterProxy pushInteger: intValue | interpreterProxy->pushInteger(intValue) |
다양한 플러그인 트릭
interpreter proxy는 primitive 플러그인을 개발하는 데 유용한 추가 기능을 다수 제공한다.
플러그인의 성공과 실패
다음 루틴은 primitive 의 실패를 확인하는 데 유용하다:
interpreterProxy failed | interpreterProxy->failed() |
interpreterProxy primitiveFail | interpreterProxy->primitiveFail() |
interpreterProxy success: aBoolean | interpreterProxy->success(aBoolean) |
첫 번째 failed 는 primitive 가 실패할 때마다 true 를 리턴한다. primitiveFail 은 primitive 가 실패했음을 확인한다. success: 는 Boolean 표현식이 false일 경우 primitive 가 실패하였음을 확인하고, 그렇지 않을 경우 상태를 변경하지 않는다.
엄격한 타입 검사
가끔은 매개변수 또는 수신자가 스몰토크의 모양을 확인하는 데 그치지 않고 명시된 Class의 것인지 검증하는 것도 바람직하다. Oop가 특정 클래스의 인스턴스인 객체를 표현하는지 확인하기 위해서는 아래를 사용하라:
interpreterProxy | interpreterProxy->isMemberOf(oop,aString) |
is: oop | |
MemberOf: aString |
Oop가 특정 클래스의 인스턴스 또는 그 서브클래스의 인스턴스에 해당하는 객체를 표현하는지 결정하려면 아래를 사용하라:
interpreterProxy | interpreterProxy->isKindOf(oop, aString) |
is: oop | |
KindOf: aString |
인스턴스 변수의 수 결정하기
Slang은 인스턴스 변수의 수를 직접 결정하는 메서드는 제공하지 않는다. 객체 내 스몰토크 인스턴스 슬롯의 총 개수를 결정하기 위한 메서드는 제공한다:
interpreterProxy slotSizeOf: oop interpreterProxy->slotSizeOf(oop)
변수가 아닌(non-variable) 객체의 경우 slotSizeOf: 는 명명된 인스턴스 변수의 총 개수를 리턴할 것이다. 하지만 변수 객체의 경우 slotSizeOf: 는 명명된 인스턴스 변수의 수에 더해 색인 가능 변수의 개수를 리턴한다. 따라서 변수 객체의 경우 명명된 인스턴스 변수의 수는 다음으로 주어진다:
(interpreterProxy slotSizeOf: oop) – (interpreterProxy sizeOf: oop)
interpreterProxy->slotSizeOf(oop) – interpreterProxy->sizeOf(oop)
대부분 변수 아닌(non-variable) 객체에는 sizeOf 가 정의되지 않기 때문에 이 표현식을 사용 시에는 주의를 기울여야 한다.
플러그인 내부에서 객체 인스턴스화하기
SmallInteger 객체는 oop 자체를 제외한 메모리를 필요로 하지 않기 때문에 말하자면 그때그때 생성이 가능하다. 다른 모든 객체는 메모리 할당이 요구된다. 일반적으로는 호출 프로시저 주변에 래퍼(wrapper)를 이용해 스몰토크의 프로미티브에 사용되는 객체를 할당하는 편이 선호되지만 때로는 primitive 내부에서 그렇게 하는 편이 편리하거나 필수적이다. interpreter proxy는 이를 위한 루틴을 제공한다. 클래스를 표현하는 oop가 주어지면 아래와 같이 해당 클래스의 객체를 인스턴스화하고 객체 헤더를 가리키는 oop를 얻을 수 있다:
interpreterProxy | interpreterProxy-> |
instantiateClass: classPointer | instantiateClassindexableSize( |
indexableSize: size | classPointer,size); |
하지만 이 연산은 주로 객체 클래스와 연관된 initialization 코드는 실행하지 않는다. Object>>basicNew 메서드와 유사하게 공간을 할당하고 모든 인스턴스 변수를 nil로 초기화할 뿐이다. 객체가 적절히 초기화되도록 확신하기 위해서는 아래를 대신 이용해도 좋다:
interpreterProxy->clone(prototype) | interpreterProxy->clone(prototype); |
그러면 객체로 clone 메시지가 전송되지만 프로토타입에 의해 참조되는 객체의 얕은 복사(shallow copy)를 실행할 것이다.
플러그인 내부에서 클래스에 대한 oop를 얻기란 꽤 간단하다. 처음에 클래스를 매개변수로서 함수에 전달할 수 있다. 아니면 oop가 주어지면 아래를 이용해 그 클래스에 대한 oop를 얻을 수도 있다:
interpreterProxy fetchClassOf: oop | interpreterProxy->fetchClassOf(oop); |
그것이 아니면 아래 메시지 중 원하는 것을 이용해 고정된 특정 클래스에 대한 oop를 직접 얻을 수 있다:
classArray classLargePositiveInteger
classBitmap classPoint
classByteArray classSemaphore
classCharacter classSmallInteger
classFloat classString
마지막으로 interpreterProxy 는 Point 클래스의 객체 생성을 촉진시키기 위한 특수 목적의 함수를 제공한다.
interpreterProxy | interpreterProxy-> |
makePointwithxValue: xValue | makePointwithxValueyValue( |
yValue: yValue | xValue, yValue); |
메모리 관리와 쓰레기 수집
아래 함수를 이용해 primitive 로부터 쓰레기 수집을 유발할 수 있다:
interpreterProxy fullGC | interpreterProxy->fullGC(); |
interpreterProxy incrementalGC | interpreterProxy->incrementalGC(); |
이는 SystemDictionary 클래스의 유사하게 명명된 메서드와 동일하다. 하지만 위의 빠른 인스턴스화를 통하거나 positive32BitIntegerOf: 와 같이 SmallInteger 가 아닌 객체를 직접 생성할 수 있는 메서드를 이용함으로써 새 객체를 인스턴스화할 때마다 쓰레기 수집이 발생할 수 있다.
쓰레기 수집이 발생하면 C 변수에 보관된 SmallInteger oop가 아닌 oop는 무효화되고 "재로딩"되어야 하는데, 스택 또는 수신자로부터 새 포인터를 얻음으로써 이루어진다. 하지만 모든 oop가 이런 방식으로 재로딩되지는 않는데, 명시적 인스턴스화에 의해 생성된 oop가 해당하겠다.
쓰레기 수집을 야기할 수 있는 연산들 사이에서 oop를 유지하기 위해 interpreterProxy는 재매핑가능한 oop 스택을 제공하는데, 이는 쓰레기 수집 도중에 재매핑될 oop로의 참조를 보유하여 oop가 나중에 재로딩될 수 있도록 한다. 스택에 값을 넣고 빼기 위해 제공하는 함수는 다음과 같다:
interpreterProxy popRemappableOop | interpreterProxy->popRemappableOop() |
interpreterProxy pushRemappableOop: | oop interpreterProxy-> pushRemappableOop(oop); |
예를 들어, 임시 변수 또는 전역 변수 oop1과 oop2가 객체에 대한 oop 참조를 보유했다면 아래 Slang 코드는 쓰레기 수집에 걸쳐 객체의 유효성을 안전하게 보호할 것이다.
interpreterProxy pushRemappableOop: oop1;
interpreterProxy pushRemappableOop: oop2;
... Smalltalk code that might result in a garbage collection ...
oop2 ←interpreterProxy popRemappableOop;
oop1 ←interpreterProxy popRemappableOop;
... references to oop1 and oop2 ...
콜백
스퀵 VM 메모리 모델에서 가장 큰 단점 중 하나는 C-언어 함수에서 해석기를 즉시 호출할 수 없다는 점이다. 따라서 콜백을 필요로 하는 애플리케이션은 어째선지 구현이 어렵다. 제한된 콜백 기능은 interpreterProxy 메서드를 이용해 비슷하게 구현할 수 있다:
interpreterProxy | interpreterProxy-> |
signalSemaphoreWithIndex: | signalSemaphoreWithIndex( |
semaIndex | semaIndex); |
이 메서드를 호출하면 Smalltalk>>registerExternalObject: 메서드를 이용해 등록된 Semaphore로 신호를 전송한다.[3] 콜백은 다음 방법으로 준비된다: (i) 콜백 코드를 호출하기 전에 Semaphore에 대기 중인 프로세스를 fork하고, (ii) VM으로 Semaphore를 등록함으로써. interpreter proxy를 이용해 C 언어 루틴으로부터 Semaphore를 시그널링하면 콜백을 트리거할 수 있다.
워드 프로세서에서 블록의 교체를 위한 플러그인
속도의 필요성
프로그래머는 데이터 구조 내 하나의 위치에서 다른 위치로 데이터 블록을 이동시켜야 하는 경우가 종종 있다. 예를 들어 워드 프로세서의 사용자는 텍스트의 블록을 기존 위치에서 다른 곳으로 이동하길 원할 수가 있다:
1. These
2. Lines
3. Out
4. Of
5. Should
6. Not
7. Be
8. Order
사용자가 3 행과 4 행을 7 행 뒤로 이동하여 "These Lines Should Not Be Out Of Order" 문장을 만들길 원할 수 있다. 이 문제는 다른 방식, 즉 두 가지 다른 크기의 워드 블록을 "교체하는" 문제, 즉 두 개의 워드 블록{Out Of}을 3개의 워드 블록{Should Not Be}로 교체하는 문제로 설명할 수 있겠다. 이 문제를 강조하는 방식에는 여러 가지가 있다. 가령 연결 리스트와 같은 데이터 구조는 이 연산의 빠른 구현을 촉진한다.
이유가 어떻든 좀 더 압축된 데이터 구조, 가령 ASCII 바이트코드의 배열이나 객체에 대한 포인터의 연속 배열을 선택하도록 제한되어 있다면 문제가 좀 더 흥미로워진다. TECO 텍스트 에디터의 저자들은 아래 단계를 실행함으로써 "These Lines Out Of Should Not Be Order" 문자열에서 같지 않은 부분을 반전시키는 방법을 고안하였다:
- 첫 번째 블록의 요소를 제자리에서 반전시킨다;
- 결과: "These Lines fO tuO Should Not Be Order"
- 두 번째 블록의 요소를 제자리에서 반전시킨다:
- 결과: "These Lines fO tuO eB toN dluohS Order"
- 첫 번째와 두 번째 블록의 요소를 반전시킨다;
- 결과: "These Lines Should Not Be Out Of Order."
이 접근법은 동일하지 않은 블록 문제를 제자리에서 블록을 반전시키는 문제로 축소시킨다. 아래 메서드를 이용해 OrderedCollection 을 확장시킴으로써 스몰토크에서 이 접근법을 코딩할 수도 있다:
swapBlockFrom: firstFrom withBlockFrom: secondFrom to: last
"Modify me so that my elements from the block beginning with index, firstFrom, up to but not including the index, secondFrom –1, are swapped with the block beginning with index, secondFrom, up to and including the index, last. Answer self."
self reverseInPlaceFrom: firstFrom to: secondFrom-1.
self reverseInPlaceFrom: secondFrom to: last.
self reverseInPlaceFrom: firstFrom to: last
reverseInPlaceFrom: from to: to
"Modify me so that my elements from the index, from, up to and including the index, to, are reversed. Assume that I am mutable. Answer self."
| temp |
0 to: to - from // 2 do:
[:index |
temp ←self at: from + index.
self at: from + index put: (self at: to-index).
self at: to-index put: temp]
이 메서드들은 Array 클래스를 비롯해 OrderedCollection 의 모든 가변형(mutable) 서브클래스와 작동이 가능하다. 아래 DoIt을 실행하면:
#(this collection out of should not be order)
swapBlockFrom: 3 withBlockFrom: 5 to: 8
which will answer:
(이 컬렉션은 순서가 어지럽혀지면 안 된다)
이러한 메서드들은 잘 작동하는 듯 보이고, Orderedcollection 의 모든 형태를 처리하기에 충분히 일반적으로 보인다. 하지만 블록이 크게 자라면 코드 속도가 눈에 띄게 느려진다. 각각이 하나의 행을 표현하는 객체들의 Array로서 문서를 유지하는 워드 프로세서를 고려해보자. 하나의 행 객체는 메모리에서 실제 텍스트의 String일 수도 있다; 이 객체는 텍스트가 발견되는 파일 또는 근접 파일에 대한 프록시를 표현하는 객체이다; 아니면 프로그램에 관련된 다른 객체일 수도 있다.
그 표현을 이용하면 swapBlockFrom:withBlockFrom:to: 로 블록 이동과 행 삽입을 구현할 수 있다. 하지만 상당히 큰 파일의 경우 (약 100,000행으로 된 파일), 삽입을 실행하는 데 수 초 정도 시간이 소요될 수 있다. 대화형 프로그램에서 일반적인 연산에 그 정도 멈춤(pause)이 인지되면 속도가 허용할 수 없을 만큼 느려질 수 있다. 이 연산을 빠르게 실행할 수 있는 다른 많은 대안책들을 비교해봐야 하지만 플러그인 가능한 primitive 가 해결책이 될지도 모른다.
단계 1: 인터페이스 설계하기
primitive 를 작성하기 전에 그것을 어떻게 호출할 것인지 고려하면 유용하겠다. 그러면 primitive 를 작성하는 요구사항이나 가정(assumption)을 더 잘 이해하도록 해준다. 문서는 객체의 Array로 표현되므로 Array>>reverseInPlaceFrom:to:[4] 를 primitive 로 오버라이드하는 방법을 고려해볼 것이다. 플러그인과 primitive 메서드를 보유하는 InterpreterPlugin을 서브클래싱함으로써 시작하겠다:
InterpreterPlugin subclass: #FlipCollectionPlugin
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'VMConstruction-Plugins'
그리고 FlipCollectionPlugin에서 primReverseFromto라 명명된 stub 메서드를 생성하고, Array 클래스로 primitive 인터페이스를 추가한다:
primitiveReverseFrom: from to: to
(1) <primitive: 'primReverseFromto' module: 'FlipCollectionPlugin'>
(2) ^FlipCollectionPlugin doPrimitive: 'primReverseFromto'
(1) 행은 호출될 primitive 와 예상되는 모듈명을 확인한다. primitive 가 적절하게 실행되면 리턴 값이 (완료 시 스택의 맨위에 있는 값) 응답될 것이다. (a) 모듈을 로딩할 수 없거나, (b) primitive 를 모듈에서 찾을 수 없거나, (c) primitive 를 실행 시 실패할 경우, (2) 행이 실행될 것이다. (2) 행은 스몰토크에서 primitive 실행을 시뮬레이트하기 위한 환경을 준비하고 실행한다. 시뮬레이션 결과는 응답으로 primitive 에게 리턴될 것이다.
이후 Array 클래스에서 reverseFrom:to: 를 primitive 호출로 오버라이드한다:
Array>>reverseFrom: from to: to
^self primitiveReverseFrom: from to: to
이로써 인터페이스가 완료되었다. 이제 primitive 를 빌드할 준비가 되었다.
단계 2: primitive 코딩하기
위에서 논한 바와 같이 primitive 는 매개변수가 없는 메서드가 되는데, 이번에는 primReverseFromto 를 호출할 것이다.
primReverseFromto
| from to rcvrOop rcvr t |
(0) self export: true.
(1) self var: #rcvr declareC: 'int *rcvr'.
(2) to ←interpreterProxy stackIntegerValue: 0.
from ←interpreterProxy stackIntegerValue: 1.
(3) rcvrOop ←interpreterProxy stackObjectValue: 2.
(4) rcvr ←self
cCoerce: (interpreterProxy firstIndexableField: rcvrOop)
to: 'int *'.
(5) interpreterProxy success: (from >= 1 and: [from+1 <= to]).
interpreterProxy success: (to <= (interpreterProxy stSizeOf: rcvrOop)).
(6) interpreterProxy failed ifTrue: [^nil].
(7) rcvr ←rcvr - 1. "adjust for 1-based indexing."
(8) 0 to: to-from/2 do:
[:index |
t ←rcvr at: from + index.
rcvr at: from + index put: (rcvr at: to-index).
rcvr at: to-index put: t].
(9) interpreterProxy pop: 3 thenPush: rcvrOop
간단하게 논의해보자면:
- (1) 행은 이 메서드에서 생성된 C 언어 함수가 내보내진(exported) public 참조가 될 것이라 확신한다. 모든 primitive 는 이 선언을 가져야 한다.
- (2) 행은 스몰토크 temp rcvr 와 관련된 C 언어 변수는 정수에 대한 포인터로 선언될 것이라 확신한다. 기본 값은 type int이다.
- (3) 행은 스택에서 임시 변수(temporaries)에 매개변수를 로딩한다. 기억나겠지만, 메서드 호출은 먼저 수신자를 스택 위에서 밀어넣은 다음 매개변수를 오름차순으로 열거한다. 따라서 to 매개변수는 스택의 최상단에 (색인 0) 위치하고, 그 다음으로 매개변수 from과 수신자가 따라온다.
- (4) 행은 수신자에 대한 oop를 (객체의 배열) temp rcvrOop에 로딩한다.
- (5) 행은 rcvrOop의 색인 가능한 첫 번째 어드레스의 "void*" 포인터에 대한 포인터로 rcvr를 로딩하고, 결과를 "int*"로 강요한다.
- (6) 행은 일부 범위 검사를 실행하여 범위 실패 시 성공 플래그를 리셋한다.
- (7) 행은 성공 플래그를 확인한다. 실패 시 스택은 현상태 그대로 남겨지므로 primitive 의 대안적 코드가 실행 가능하다.
- (8) 행은 1부터 시작하는 색인(1-based indexing)을 위해 rcvr를 조정한다. C 언어 배열은 0부터 시작되므로 잇따른 참조는 포인터를 조정함으로써 배열을 1부터 시작되는 스몰토크 배열로 취급할 수 있다.
- (9) 행은 실제 작업을 실행한다. 이는 self 가 rcvr 로 대체되었다는 점만 제외하면 OrderedCollection>>reverseFrom:to: 의 예제 코드에 제시된 코드와 동일하다. (7행이 없다면 코드는 C의 0-indexed 배열용으로 수정되어야 했을 것이다.)
- (10) 행은 스택으로부터 매개변수와 수신자 oop을 꺼내고, 수신자를 표현하는 oop를 다시 밀어넣을 것이다 (rcvr를 리턴하길 원하므로 단순한 pop: 2로 충분할 것이다).
단계 3: 플러그인 빌드하기
primitive 를 코딩했으니 비록 슬로우 모션이지만 플러그인 해석기를 이용해 이 코드를 직접 테스트할 수 있다. 플러그인이 설치되지 않았기 때문에 primReverseInPlaceFromto 에서 primitive 호출이 실패하고 해석기를 호출할 것이다. 플러그인을 실제로 컴파일 및 설치하지 않고 이런 방식으로 대부분의 플러그인 코드를 테스트할 수 있다.
코드가 올바르게 작동하고 있다고 만족하면 플러그인을 빌드할 수 있다. 잇따른 doIt은 플러그인에 상응하는 C 언어 파일을 생성할 것이다:
FlipCollectionPlugin translate
이후 생성된 파일은 공유 라이브러리와 같은 네이티브 시스템 툴을 이용해 컴파일 가능하고, 라이브 시스템 코드로 테스트하기 위한 플러그인으로서 설치 가능하여 블록 이동 루틴에서 상당한 속도 증가를 야기한다.
Notes
- ↑ 번역된 C 코드는 하나 또는 그 이상의 include 파일에 따라 좌우되는데, 이 파일은 표준 스몰토크 이미지에 보관된 내용의 복사본이다. 이러한 파일은 워크스페이스에 “InterpreterSupportCode writePluginSupportFiles”라는 표현식을 실행하면 텍스트 파일로 생성할 수 있다.
- ↑ LargePositiveInteger oop가 #positive32BitIntegerOf: 로 호출 도중에 생성될 경우 쓰레기 수집이 발생하여 다른 C 변수에 보관된 oops를 무효화(invalidate)할지도 모른다. 메모리 관리와 쓰레기 수집에 관한 내용을 아래에서 참고하라.
- ↑ #signalSemaphoreWithIndex: 메서드는 신호를 즉시 전송하진 않지만 VM으로 신호에 대한 요청을 등록한다. VM은 primitive 가 VM으로 제어를 리턴한 직후 신호를 전송할 것이다.
- ↑ 이번 경우 블록 이동이 reverseInPlaceFrom:to: 메서드에서 그 시간을 대부분 소요하는 듯 보이지만 일반적인 경우엔 이렇게 명백하지 않다. 스퀵은 MessageTally 와 같이 성능의 프로파일링을 위한 훌륭한 툴을 제공하는데, 이러한 측정은 종종 플러그인이 어떻게 설계되어야 하는지를 통지할 것이다.