Unity 개발을 위한 C# 기본 내용 정리
C# 기본 내용을 확실히 정립하기 위해한 자료 조사
2023.02.15
목차
그동안 C# 기본 내용을 잘 모른채 Unity 개발에 임해왔다는 생각이 들어,
기본기를 확실히 정립하고자 내용을 정리해봄
나중에 취업준비 할 때도 쓰면 좋을듯!
C#
- .NET, CLR, IL, JIT, AOT
- Managed code, Unmanaged code, GC, using
- Boxing, Unboxing
- Assembly, EXE, DLL
Unity
- IL2CPP, Mono,
- .NET Framework, .NET Core, Xamarin, .NET
Android
- JDN, SDK, NDK
- Call Java and Kotlin plug-in code from C# scripts
- JNI, AndroidJavaObject, AndroidJavaClass
iOS
- Call Cpp and Objective-C plug-in code from C# scripts
- m file, mm file
- Calling C# code back from native code
C#
C#은 마이크로소프트에서 만든 객체 지향 프로그래밍 언어이며, .NET Framework에서 사용되는 주된 언어이다
2000년대 들어서며 오라클의 자바가 새로운 차세대 언어로 각광받으면서 MS에서도 자바의 장점을 수용하여 새로운 언어를 만들어 출시했는데, 이것이 C#이다.
1995년에 등장한 JAVA에서 영향을 많이 받았으며, 다음과 같은 특징이 있다.
- Object-Oriented Programming Language : 객체 지향 언어
- Strongly Typed Language : 강한 타입화 언어
- Compiled Language : 컴파일된 언어
- Managed Language : 자동 메모리 관리가 되는 언어
.NET, CLR, IL, JIT, AOT
🔸 C#의 컴파일 과정
C#은 컴파일 언어로서 C# 스크립트를 컴파일하면 중간언어인 IL
(Intermediate Language, =MSIL, =CIL)로 변환된다.
이를 .NET assembly라고도 하는데, Java의 JVM과 같이, C#에서 컴파일된 IL은 CLR에서 Native code로 변환되어 실행된다.
사용자가 이 .NET Assembly, 즉 컴파일된 중간 언어 파일인 .exe를 실행하면 메모리에 CLR이 로드되고,
해당 .exe를 분석하게 되고 해당 OS/아키텍처에 맞는 코드(기계어)로 컴파일 되어 실행되게 된다. 이를 JIT(Just-In-Time) 컴파일 이라고 한다.
이 모든 과정은 .NET Framework에서 이루어지며, C# 뿐 아니라 VB.NET C++.NET F# 등등 다양한 언어도 같은 과정으로 컴파일이 된다.
🔸 JIT vs AoT
- Just-In-Time 컴파일 : 실행 전이 아니라 프로그램 실행 중에 컴파일과 관련된 컴퓨터 코드를 실행하는 방법
- Ahead-Of-Time 컴파일 : 프로그램을 실행하기 전에 기계어 코드로 변환하여 컴파일하는 컴파일러
AOT 컴파일은 이미 실행하기 전에 Native코드로 변환이 끝나있으므로, JIT 방식 보다 실행 속도가 빠르다는 장점이 있다.
JIT 컴파일은 미리 컴파일된 중간 언어를 실행 환경에 맞도록 한번 더 컴파일 하므로, 이식성이 뛰어나다.
C#과 같이 JIT 컴파일로 동작하는 Java의 동작 모습 (적당한 삽화가 없어 Java로 대체)
JIT 컴파일 방식은 실행시 컴파일을 진행하므로 실행 할 때 느리며, 컴파일 에러가 발생 할 수 있다.
🔸 .NET Framework
.NET Framework는 Windows에서 애플리케이션을 빌드하고 실행하기 위한 소프트웨어 개발 프레임워크이다.
데스크톱 응용 프로그램에서 웹 응용 프로그램에 이르기까지 광범위한 응용 프로그램을 구축하기 위한 강력하고 유연한 플랫폼을 제공한다.
.NET의 아키텍처는 위와 같으며, 상세한 설명은 아래에 기술하였다.
🔹 CLS (Common Language Specification)
CLS는 .NET 프레임워크에서 실행되는 모든 언어가 따라야 하는 일련의 규칙과 제한 사항을 정의한 공통 언어 규격이다.
CLS를 따르는 코드는 모든 .NET 언어에서 컴파일 및 실행이 가능하며, CLS에 정의된 기능과 유형만 사용할 수 있다.
이렇게 함으로써 .NET Framework에서 다양한 언어를 사용하는 개발자들이 서로 다른 언어를 사용하더라도 코드를 공유하고 사용할 수 있게 된다.
예를 들어, C#이 따르는 CLS의 몇 가지 예를 들면 다음과 같다:
- 메서드와 속성 이름은 유일해야 한다.
- 파라미터 타입과 반환 타입은 CLS 규격에 명시된 기본 형식을 사용해야 한다.
따라서 CLS를 준수하는 코드는 다양한 .NET 언어에서 컴파일 및 실행될 수 있다.
🔹 CTS (Common Type System)
CTS는 .NET 호환 언어가 지켜야할 타입의 표준 규격이다.
다시 말해, C#을 비록한 모든 .NET framework를 지원하는 언어들이라면 반드시 따라야하는 데이터 형식 표준이다.
C#에서는 4바이트 정수를 int라고 사용하고, VisualBasic.NET에서는 Integer라고 사용한다.
하지만 이들은 모두 .NET의공통 타입인 System.Int32 타입이다.
🔹 FCL (.NET framework Class Library)
.NET Framework를 통해 개발할 수 있는 모든 응용 프로그램에 대한 공통 클래스 라이브러리. FCL은 BCL(Basic Class Library)를 포함하고 있으며,
이 API 모음은 다양한 프로그래밍 작업을 수행할 수 있는 클래스, 인터페이스, 구조체 등등을 포함하고 있다.
FCL은 .NET Framework에서 개발자가 일반적으로 필요로 하는 기본적인 기능을 제공한다.
예를 들어, 문자열, 날짜 및 시간, 파일 및 디렉토리 작업, 네트워크 통신, 데이터 액세스 등의 작업을 수행할 수 있는 클래스와 메서드를 포함한다.
🔹 ASP.NET
ASP.NET은 Microsoft가 개발한 웹 응용 프로그램 개발을 위한 라이브러리이다.
동적 웹 페이지, 웹 어플리케이션, 웹 서비스 및 모바일 애플리케이션 등 다양한 유형의 웹 애플리케이션 개발을 지원하며,
HTML, CSS, JavaScript와 같은 웹 기술과 함께 사용된다.
🔹 ADO.NET
ADO.NET은 데이터베이스와의 상호 작용을 위한 데이터 액세스 기술을 제공하는 라이브러리이다.
클래스와 인터페이스의 형태로 데이터에 접근 하는 방법을 제공한다.
ADO.NET을 사용하면 데이터베이스 관련 작업을 수행할 수 있다.
- 예 ) 데이터베이스 연결, SQL 실행, 검색 및 업데이트, 데이터 스키마 정의 등
🔹 Windows Forms, Console
Windows Forms는 .NET Framework에서 GUI (Graphical User Interface)를 개발하기 위한 라이브러리 중 하나이다.
이를 통해 개발자가 직접 폼(Form), 컨트롤, 대화 상자 및 메뉴와 같은 UI요소를 디자인하거나 만들 수 있으며,
각 UI 요소들에 대해 이벤트 핸들러 등록이나 데이터 바인딩을 위한 AP를 제공한다.
Windows Console은 콘솔 창(CMD)을 통해 사용자와 상호작용하는 응용 프로그램을 만드는 라이브러리 중 하나이다.
🔹 .NET Remoting
.NET Remoting은 .NET Framework에서 다른 프로세스나 컴퓨터에 있는 객체 간 통신을 가능하게 하는 라이브러리이다.
원격 호출(Remote Procedure Call, RPC)을 기반으로 하며, 다른 프로세스 또는 컴퓨터에 있는 객체를 마치 로컬 객체처럼 사용할 수 있다.
이를 통해 분산 애플리케이션을 구축하기 위할 수 있으며, TCP/IP, HTTP등을 사용하여 원격 애플리케이션간의 통신을 가능하게 한다.
🔹 CLR (Common Language Runtime)
.NET Framework의 런타임
CLR은 IL언어로 작성된 코드를 실행하기 위한 가상 머신으로, .NET Framework의 가장 중요한 요소중 하나임.
마치 Java에서 Bytecode를 읽어 어느 환경에서든 실행시켜주는 JVM처럼, .NET Framework에서는 IL을 읽어 어느 환경에서든 실행시켜주는 CLR이 있다.
그렇다면 CLR을 사용함으로써 개발자가 얻을 수 있는 장점은 무엇일까?
번호 | 장점 | 설명 |
---|---|---|
1 | 플랫폼 독립성 | CLR은 플랫폼 독립적인 코드 실행을 지원한다. 즉, 한 번 작성한 코드를 여러 플랫폼에서 실행할 수 있다. |
2 | 예외 처리 | CLR은 예외 처리를 지원한다. 개발자는 예외가 발생한 경우 이를 적절하게 처리할 수 있다. |
3 | Garbage 처리 | CLR은 Garbage Collection 기능을 제공한다. 이 기능은 사용되지 않는 객체를 자동으로 제거하여 메모리를 해제한다. |
4 | 보안 | CLR은 코드 액세스 보안을 제공한다. 실행되는 코드가 사용자 시스템에 영향을 미칠 수 있는 작업을 수행하지 않도록 제한한다. |
다만 찾아보니 4번 장점 중 코드 엑세스 보안 (CAS) 는 실행될 코드에 권한을 부여하고,
권한 밖의 작업은 실행되지 못하도록 하는 것인데, 지금은 더 이상 사용하지 않는 것으로 보인다. (링크)
🔹 CLI (Common Language Infrastructure)
마이크로소프트에서 ECMA 표준으로 제출한 공개 규약.
CLI 는 CTS 명세를 포함하며 IL에 대한 코드의 구조 표중 사양으로 기술한다.
CLI 규격을 마이크로소프트에서 구현한것이 CLR 이다.
- 📗 참고 자료
Managed code, Unmanaged code, GC, using
🔸 Managed code, Unmanaged code
.NET 기반의 환경을 관리되는(Managed) 환경으로 부르곤 한다. 여기서 관리되는 주체는 바로 ‘메모리’이다.
C,C++등등은 직접 new연산자와 malloc함수를 이용하여, 힙(Heap)영역에 동적메모리를 할당하고
delete 연산자와 free함수를 이용해서 동적 메모리를 해제하는 부분을 개발자가 직접 처리해야만 했다.
하지만 C#과 같은 Managed Language에서는 메모리 관리와 같은 낮은 수준의 작업을 개발자 대신 CLR이 자동으로 처리한다.
이를 통해 개발자는 더 높은 수준의 추상화를 제공하는 프로그래밍을 할 수 있으며, 코드의 안정성이 향상될 수 있다.
정리하면 다음과 같다.
- Managed code : 관리 코드란 런타임에서 실행이 관리되는 코드.
- Unmanaged code : 메모리 관리 및 시스템 자원에 직접 접근할 수 있는 코드.
C#의 CLR은 Managed code의 메모리 관리를 구현하기 위해 Garbage Collector를 사용한다.
🔸 Garbage Collection in C#
C#으로 작성한 소스 코드를 컴파일해서 실행 파일을 만들고 실행하면, CLR은 이 프로그램을 위한 일정 크기의 메모리를 확보한다.
이를 **관리되는 힙 (Managed Heap)**이라고 한다.
CLR은 확보한 힙 메모리의 첫번째 주소에 "다음 객체를 할당할 메모리의 포인터"
를 위치시킨다.
만약 아래 코드에서처럼 레퍼런스 타입 변수에 객체를 할당한다면, 할당된 객체가 관리되는 힙에 생성된다.
if(true) {
object a = new object();
}
이때, 변수 a처럼 할당된 메모리의 위치를 참조하는 객체를 **루트, Root**라고 한다.
루트는 스택에 생성될 수도 있고, 정적 필드 (Static)처럼 힙에 생성될수도 있다.
.NET 어플리케이션이 실행되면 JIT 컴파일러가 이 루트들을 목록으로 만들고, CLR은 이 루트 목록을 관리하며 상태를 갱신한다.
루트가 중요한 이유 : Garbage Collector가 CLR이 관리하던 루트 목록을 참고해서 Collection을 하기 때문이다.
Collection은 루트 목록을 순회하면서 힙 객체와의 관계를 조사하는데, 다음의 간단한 원리로 판단한다.
- 어떤 힙과도 루트 관계가 없다면 사용하지 않는
Garbage
이다.
Collection이 이루어지는 과정은 다음과 같다.
- 일단 시작하면 Garbage collector는 모든 객체가 Garbage라고 가정한다.
- 루트 목록을 돌면서, 참조 관계를 확인한다. 만약 루트가 참조하고 있는 객체라면, Garbage목록에서 제외한다.
- 만약 루트에서 참고하고 있는 힙의 객체가 또 다른 힙의 객체를 참조하고 있다면, 그 객체도 루트가 참조하고 있다고 간주해 목록에서 제외한다.
- 아무 참조가 없는 객체를 관리되는 힙에서 제거한다. (빈 공간이 생김)
- 빈공간을 없애기 위해 메모리를 정렬하고, 다음 노드를 할당할 메모리의 포인터를 그 끝으로 옮긴다.
1 | 2 | 3 |
---|---|---|
또한 CLR에서는 객체가 생성된지 얼마 안될수록 Garbage일 확률이 높고, 오래됐을 수록 Garbage가 아닐 확률이 높다고 간주한다. C#에서는 객체가 GC를 겪은 횟수에 따라 세대를 분류한다.
- 0세대 : GC를 경험하지 않은 객체
- 1세대 : 1번 GC를 겪고 살아 남은 객체
- 2세대 : 2번 이상 GC를 겪고 살아 남은 객체
0세대에서 GC가 수행될 경우,
1세대와 2세대의 GC는 수행하지 않는다.
1세대에서 GC가 수행될 경우,
0세대도 포함해서 GC를 진행하고, 2세대의 GC는 수행하지 않는다.
2세대에서 GC가 수행될 경우,
0세대와 1세대도 포함해서 GC를 진행한다. (Full GC)
마지막으로, GC는 언제 수행될까?
- 각 세대별로 객체가 할당 가능한 임계치를 넘어갈 때
GC.Collect
메서드를 호출 할 때- 시스템의 메모리가 부족할 때
Garbage collection이 진행 될 때 CLR은 어플리케이션의 실행을 잠시 멈추고 여유 메모리를 확보하려 하므로, 너무 잦은 GC의 실행은 전체적인 성능 저하 문제로 이어 질 수 있다. 따라서 다음과 같은 사항을 명심해야 한다.
- 객체를 너무 많이 할당하지 않기
- 너무 큰 객체 할당을 피하기 (85KB, LOH(Large Object Heap)의 메모리 정리 불가 이슈)
- 너무 복잡한 참조 관계를 만들지 않기 (메모리 정리후 메모리 값 수정 이슈)
- 루트를 너무 많이 만들지 않기
🔸using
기본적으로 .NET framework는 Garbage Collector에 의해서 모든 리소스를 자동으로 관리한다.
하지만 중요한 맹점이 하나 있는데, .NET framework 외부의 비관리 영역상의 자원에 대해서는 적용되지 않는다는 것이다.
파일 핸들, 메모리 핸들, 데이터베이스 연결, 소켓 핸들, 스레드 핸들 등이 비관리 영역상의 자원, Unmanaged Resource이다.
따라서 이런 자원들을 메모리에서 해제하기 위한 명시적인 소멸자를 직접 구현할 수 있는데, 이때 사용하는 것이 IDisposable
인터페이스이다.
IDisposable
인터페이스를 구현한 객체는 Dispose
메서드를 통해 해제할 수 있다.
IDisposable myObj = new SomethingDisposable();
try { /* try something here with myObj */ }
catch { throw; }
finally { if (myObj != null) myObj.Dispose(); }
이때, 위의 try-catch-finally
문을 간단히 한 것이 using
인데, 다음과 같이 작성 할 수 있다.
using (IDisposable myObj = new SomethingDisposable()) {
/* try something here with myObj */
}
using
에서 할당한 IDisposable
객체는 구문이 끝나면 자동으로 Dispose
된다.
Boxing/Unboxing
🔸 Boxing/Unboxing
C#에서 Boxing과 Unboxing은 값 형식(Value Type)과 참조 형식(Reference Type) 간의 변환을 의미한다.
- Boxing :
Value type을 Reference type으로 변환하는 것
. 이 과정에서 Value type은 Heap 영역에 할당되고, 이를 참조 변수로 지정하여 Reference type으로 사용할 수 있다.Boxing
은 묵시적으로 일어나며 명시적으로 해도 무방하다. - Unboxing : 반대로
Reference type을 Value type으로 변환하는 것
. 이 과정에서 Heap 영역에 있는 Value type 데이터를 Stack 영역에 할당하게 된다. 추가로, Boxing한 데이터에 한해 Unboxing을 할 수 있다.
다음 문장은 Boxing
및 Unboxing
의 결과를 모두 보여준다.
int i = 123; // a value type
object o = i; // boxing
int j = (int)o; // unboxing
Value type
<-> Reference type
을 오가는 Boxing
과 Unboxing
은 비용이 많이 드는 작업임을 유념해야 한다.
Boxing
의 경우 완전히 새로운 객체를 할당하고 구성해야 하며,
Unboxing
에 필요한 캐스팅 연산 또한 상당한 계산 과정이 필요하다고 한다.
MSDN에서는 다음과 같이 설명하고 있다:
값 형식이 boxing되면 완전히 새로운 개체가 생성되어야 합니다. 이 작업은 단순 참조 할당보다 20배나 오래 걸립니다. unboxing 시 캐스팅 프로세스는 할당의 4배에 달하는 시간이 소요될 수 있습니다.
이렇게 성능이 좋지 못한 Boxing
/ Unboxing
을 사용하는 이유는, 사용상의 편의성이 있기 때문이다.
C#의 모든 자료형을 포함한 객체들은 System.Object
를 상속 받으므로, 데이터를 System.Object
로 간주할 경우
타입으로 인한 제약 사항에서 매우 자유로워지게 된다. (아래 코드 처럼…)
System.Collections.ArrayList al = new System.Collection.ArrayList();
al.Add(123); // 정수 추가
al.Add("Apple"); // 문자열 추가
al.Add(new MyClass()); // 객체 추가
위 코드에서 Add
메서드가 호출 될 때 마다 Boxing
이 일어나고, 실제로 사용하기 위해서는 가져올 때 해당 타입으로 Unboxing
을 해줘야 한다.
object o = al[0];
int j = (int) o; // Unboxing
Boxing
/ Unboxing
은 편리하기는 하지만 느리다는 단점이 있으며,
boxing되기 전 자료형으로 명확히 캐스팅을 해줘야 하기 때문에 형식(타입)에 대한 불안정성도 증가하게 된다.
.NET 2.0부터는 이러한 Boxing
/ Unboxing을사용하는 ArrayList
의 단점인 고비용과 형식 불안정성을 개선하기 위해 Generic
컬렉션을 제공하게 되었다. (링크)
예를 들어, ArrayList
로 숫자를 담는 리스트를 만들면 다음과 같다.
System.Collections.ArrayList al = new System.Collection.ArrayList();
al.Add(123); // Boxing
al.Add(456); // Boxing
al.Add(789); // Boxing
int a = (int)al[1]; // Unboxing
하지만, 이를 Generic
을 사용한 List<T>
클래스로 구현하면 다음과 같다.
List<int> list = new List<int>();
list.Add(123); // Boxing 아님
list.Add(456); // Boxing 아님
list.Add(789); // Boxing 아님
int j = list[0]; // Unboxing 아님
- 📗 참고 자료
Assembly, EXE, DLL
🔸 Assembly
Assembly는 .NET / .NET Framework 환경에서 배포 및 실행되는 코드와 리소스의 묶음이다.
C#에서 Assembly에는 DLL 파일이나 EXE 파일이 있다.
Assembly는 C# 코드를 컴파일하여 생성할 수 있다.
.NET 컴파일러는 컴파일한 코드와 해당 코드에서 참조하는 모든 리소스를 단일 파일로 묶어서 생성한다.
이렇게 생성된 파일은 .NET 환경에서 실행되며, Assembly에는 코드뿐만 아니라 어셈블리 메타데이터, 리소스, 버전 정보 등이 포함될 수 있다.
그림처럼 Assembly는 크게 Manifest
, Type Metadata
, CIL Code
로 이루어져 있다.
Manifest
는 Assembly 자체 정보를 갖는 메타 데이터이다.
Assembly의 ID(이름 및 버전)나 이 Assembly가 또 다른 어떤 Assembly를 참조하는지 등등 여러가지 정보가 포함된다.
Type Metadata
에는 Assembly에서 사용하는 모든 형식(Type)에 대한 정보를 담고 있다.
프로그램안에서 리플렉션을 사용하여 Asssembly에 대한 정보를 읽을 수 있는데, 이 metadata를 참고해 동작한다.
CIL Code
는 실제로 동작하게 되는, CLR의 JIT 컴파일러에 의해 읽히고 Native code로 변환되는 코드들이다.
🔸 EXE, DLL
exe와 dll은 모두 Windows 운영체제에서 사용되는 실행 파일의 형식이다.
exe는 executable으로,
프로그램을 실행하는 데 필요한 모든 코드와 데이터를 포함하는 실행 파일이다.
보통은 하나의 애플리케이션을 담고 있으며, 사용자가 직접 실행할 수 있다.
반면에 dll은 dynamic link library로,
여러 프로그램에서 공유할 수 있는 함수, 클래스, 데이터 등을 담고 있는 라이브러리 파일이다.
다른 프로그램에서 이 파일에 있는 함수나 클래스를 호출하여 사용할 수 있다. dll은 실행 파일에 포함되지 않으며, 필요할 때 로드되어 사용된다.
따라서 exe는 독립적으로 실행되는 프로그램을 위한 실행 파일이며, dll은 여러 프로그램에서 공유할 수 있는 라이브러리 파일이다.
- 📗 참고 자료
Unity
2004년 8월 Unity Technologies가 개발한 게임 엔진이다.
IL2CPP, Mono
🔸 Mono (JIT)
C#은 기본적으로 위와 같은 컴파일 동작 & 런타임 동작을 가진다.
C# 소스코드는 대상 플랫폼에서 동작하기 위해 .NET Framework의 CLR에서 실행 가능한 IL 코드로 컴파일 되는데,
.NET Framework는 Windows 플랫폼만을 대상으로 했기 때문에 다양한 플랫폼(Android, iOS)을 대상으로 빌드가 불가능했다.
그래서 나온 것이 바로 Mono
이다.
Mono는 MS의 .NET Framework의 오픈 소스 구현이다.
기본적으로 C# 컴파일러와 CLR을 포함하며, Windows 뿐 아니라 MacOS, Android, iOS, Xbox, PlayStation 등 다양한 환경에서 동작한다.
현재는 MS에서 정식으로 인수하여 Xamarin 프로젝트로서 개발되고 있다.
Unity는 크로스 플랫폼 개발을 지원하기 위해 Mono를 스크립팅 백엔드로 사용했다.
따라서 게임 개발자는 게임 코드를 한 번 작성하면 거의 모든 플랫폼에 이를 배포할 수 있게 되었다.
결국, 스크립팅 백엔드를 Mono로 설장하면 개발자가 C#으로 작성한 코드가 IL로 변환어 배포 되고,
Mono상에서 이를 런타임에서 읽어들이는 JIT 방식으로 컴파일하여 Android나 iOS에서도 동작하게 되는 것이다.
🔸 IL2CPP (AOT)
하지만 Mono에는 단점이 존재한다.
- C# runtime performance가 C/C++에 비해 여전히 느리다.
- 더 뛰어난 최신 버전의 .NET 언어를 Unity Mono에서 지원하기 힘들다.
- 다양한 플랫폼들의 아키텍처에 맞추려면 포팅, 유지보수 등의 작업이 필요한데 너무 많은 노력이 든다.
이를 해결하기 위해 Unityd에서 새로운 스크립팅 백엔드를 제안하는데, 이것이 IL2CPP이다.
IL2CPP는, Unity에서 만든 새로운 스크립팅 백엔드이다.
IL2CPP는 다음과 같은 2가지 큰 특징을 가지고 있다.
- AOT(ahead-of-time) 컴파일러
- 가상 머신을 지원하는 런타임 라이브러리
IL2CPP는 IL로 변환된 C#코드를 CPP로 변환하는 과정을 진행하며, 이 과정이 AOT 컴파일이다.
CPP코드는 다양한 환경에서 동작할 수 있는데, WebGL의 경우 asm.js에서 C나 CPP코드를 가져올 수 있고,
iOS의 경우에도 Xcode에서 CPP를 지원하며, Android의 경우 NDK를 사용하여 C나 CPP코드를 사용 할 수 있다.
IL2CPP 기술의 다른 부분은 가상 머신을 지원하는 런타임 라이브러리이다.
이 라이브러리는 C++ 코드를 사용하여 구현했으며, libil2cpp
라고 부른다.
libil2cpp
는 Garbage Collector, 스레드 및 파일 핸들링과 같은 서비스 와 인터페이스를 제공한다.
정리하면,
IL2CPP는 코드를 CPP(Native)로 변환함으로써 많은 플랫폼을 대상으로 빠른 속도를 보장한다는 장점이 있다.
또한 빌드 결과물들이 CPP로 변환된 후, C++ 컴파일러에 전달되어 각 플랫폼별로 Native 언어로 변경되므로,
빌드 결과물을 열어보면 기계어로 작성이 되어 있어 코드가 난독화된다.
Mono는 중간 언어로만 변환후 배포하기 때문에 IL2CPP에 비해 상대적으로 속도가 빠르다는 장점이 있다.
- 📗 참고 자료
.NET Framework, .NET Core, Xamarin, .NET
🔸 .NET Core, Xamarin
.NET은 본래 Windows 플랫폼만 지원하였는데,
크로스 플랫폼과 모바일 플랫폼까지 지원하기 위해 두 프레임워크가 더 추가되었다.
- .NET Framework(닷넷 프레임 워크)
- 윈도우 전용
- 다양한 기능과 확장을 지원
- .NET Core(닷넷 코어)
- 윈도우, 리눅스, macOS 에서 사용가능
- .NET Framework를 경량화 하여 매우 가벼움
- Xamarin
- 모바일 환경에 제한된 기능
- 안드로이드, IOS, 윈도우 모바일(UWP) 등을 지원
🔸 .NET Standard
As-Is | To-Be (.NET 2.0) |
---|---|
- 더 많은 API를 사용할 수 있습니다.
.NET 1.6에서 1.3k의 정도의 API를 사용할 수 있었지만, .NET 2.0은 32k의 API 사용이 가능하다.
- .NET Framework와 호환성이 좋다.
대부분의 NuGet package들이 .NET Framework을 대상으로 만들어져 있다.
따라서, 호환성을 높여서 .NET Standard 2.0을 이용할 경우 NuGet package의 70% 정도를 그대로 사용할 수 있다.
이는 개발자가 닷넷 스탠다드 API만을 사용하여 라이브러리를 빌드해 배포하면
해당 닷넷 스탠다드 버전을 지원하는 닷넷 환경에서는 어디서든 참조 및 실행이 가능하다는 것이다.
그러므로 다양한 플랫폼에서 공통적으로 사용하기 위한 라이브러리는 닷넷 스탠다드를 타깃으로 개발할 것을 권장하고 있다.
- NuGet : 마이크로소프트에서 만든, 마이크로소프트 개발 플랫폼에서 쓰이는 무료 또는 오픈소스의 패키지 관리자이다.
🔸 .NET
.NET은 MS가 2020년 선보인 모든 앱을 빌드 가능한 오픈 소스 개발 플랫폼이다.
2020년 MS는 닷넷 프레임워크 4.8이 닷넷 프레임워크의 마지막 릴리스라고 발표하며, .NET을 공개했다.
그동안 닷넷 프레임워크와 닷넷 코어로 나누어 지원하던 플랫폼을 닷넷(.NET)이라는 단일 이름으로 통일하기로 한 것이다.
시작 버전이 5.0인 것은 닷넷 코어의 최신 버전이 3.x이고 닷넷 프레임워크의 최신 버전이 4.8인 탓에
개발자 혼란을 줄이기 위해 이 둘을 아우르는 버전을 채택한 것이라고 한다.
🔸 Unity에서
Unity 2017.1 릴리스부터, `.NET 4.x` 또는 `.NET Standard 2.0` 스크립팅 런타임을 사용할 수 있다.
Android
JDK, SDK, NDK, JNI
🔸 JDK (Java Development Kit)
Java Application을 구축하기 위한 핵심 플랫폼의 구성 요소.
자바 기반 소프트웨어를 개발하기 위한 도구들로 이뤄진 패키지이다.
- JVM (Java Virtual Machine)
- JRE (Java Runtime Environment)
🔸 SDK (Software Development Kit)
UI 기반으로 특화된 API를 제공하여 Application Level에서 개발이 가능하게 도와주는 패키지
Java 기반으로 Application Emulator를 내장하고 있어 SDK 설치시 Build 와 Test가 가능하다.
UI 기반의 안드로이드 애플리케이션을 개발할 때 활용됨
🔸 NDK (Native Development Kit)
C/C++을 이용하여 애플리케이션, 미들웨어 개발에 사용되는 Framework
SDK를 토대로 만든 안드로이드 애플리케이션은 자바를 활용했기 때문에 자바의 한계점을 그대로 가지고 있다.
(그래픽 처리나 시그널 프로세싱(센서 값 처리) 등 CPU의 처리 속도가 중요한 부분에서 SDK의 한계점이 드러남)
이런 처리를 위해 구글에서 안드로이드 애플리케이션에서도 C/C++을 활용할 수 있도록 제공하는 도구가 바로 NDK이다.
단, JVM 위에서 실행되고 있는 Java 코드와 NDK를 통해 C,C++등 다른 언어로 작성된 코드가 서로 상호작용 할 수 있는 이유는 JNI
때문이다.
🔸 JNI (Java Native Interface)
안드로이드 SDK를 토대로 만든 안드로이드 애플리케이션은 달빅 가상 머신(Dalvik Virtual Machine) 위에서 동작하는 자바 기반의 프로그램이다.
때문에 C/C++로 생성한 애플리케이션에 비해 느린 실행 속도 등 자바가 지닌 여러 한계를 그대로 가지고 있다.
해당 플랫폼의 고유기능(Native)을 Java로 해결 하는 것이 아닌 운영체제에서 사용되는 언어(보통 C, C++)로 플랫폼의 고유기능을 만든다.
JNI는 C,C++로 만들어진 고유기능 즉, 함수를 Java 메서드와 연결해주는 역할을 하게 되고,
그로인하여 Java 메서드를 호출 할 시, C나 C++로 작성된 함수가 실행 되게 된다.
Java에서 C 라이브러리 함수 호출
- 자바 클래스에 네이티브 메서드 선언 (
System.loadLibrary()
메서드를 호출해서 C 라이브러리 로딩)
- Java 코드 컴파일 (JDK 8버전부터 Javah가 Javac로 대체되었음)
- C 헤더 파일 생성
- C 코드 작성 (JNI Native 함수 구현, 실제로 동작하는 부분)
- C 라이브러리 생성
- Java 프로그램 실행 (JNI를 통한 Native 함수 호출)
**C 코드에서 Java 메서드 호출** C/C++에서 Java의 클래스를 이용하기 위해서는 Reflection을 이용해야 한다.
- 자바 가상 머신 생성
- 실행할 클래스 검색 후 로드
- 해당 메서드 ID 획득
- 클래스 메서드의 인자로 넘겨줄 객체 생성
- 메서드 호출
- 📗 참고 자료
Call Java and Kotlin plug-in code from C# scripts
🔸 JNI, AndroidJavaObject, AndroidJavaClass
Unity는 JNI를 사용하여 Java 코드와 상호 작용하는 데 사용할 수 있는 low-level
및 high-level
API를 모두 제공한다.
- low-level : 저수준의
AndroidJNI
Class를 래핑하고 있는AndroidJNIHelper
API를 사용함. - high-level : 고수준의
AndroidJavaObject
,AndroidJavaClass
,AndroidJavaProxy
API를 사용함.
고 수준의 API들은 JNI 호출에 필요한 많은 작업을 손쉽게 처리할 수 있도록 도와준다.
특히, AndroidJavaObject
와 AndroidJavaClass
는 AndroidJNI
, AndroidJNIHelper
를 기반으로 구현되었다.
AndroidJavaClass 클래스는 Java 클래스를 로드하고 호출하기 위한 클래스이다.
이 클래스를 사용하여 Java 클래스의 생성자를 호출하고 클래스의 정적 메서드를 호출할 수 있다.
AndroidJavaObject 클래스는 Java 개체를 호출하기 위한 클래스이다.
이 클래스를 사용하여 Java 객체의 메서드 및 필드를 호출할 수 있습니다.
TO-DO : Android - Unity Plugin의 호출 과정을 그림으로 도식화하기
, JAR, AAR 차이점 정리하기
- 📗 참고 자료
iOS
Call Cpp and Objective-C plug-in code from C# scripts
🔸 m file, mm file
Objective-C에는 다음 확장자들이 사용된다.
- .h 파일 : Objective-C 헤더 파일
- .m 파일 : Objective-C 구현 파일
- .mm 파일 : Objective-C, C++ 혼용 가능 파일
확장자가 mm으로된 파일은 Objective-C 문법과 C++ 문법 둘 다 가능하다.
Unity iOS에서 Swift 또는 Objective-C 코드를 사용하려면 Xcode에서 iOS 플러그인을 만들어야 합니다.
유니티에서 네이티브로 함수 호출을 하려면 두 가지 셋업을 해야 한다.
- Objective-C : extern “C”로 함수 노출
- Unity3D(C#) : [DllImport(“__Internal”)] 어트리뷰트를 통해 네이티브로 함수 연결
🔸Calling C# code back from native code
Unity iOS는 UnitySendMessage
를 통한 콜백 기능을 지원한다.
다음과 같이 사용이 가능하다.
UnitySendMessage("GameObjectName1", "MethodName1", "Message to send");
이 함수에는 대상 GameObject의 이름, 호출할 스크립트 메서드, 호출된 메서드에 전달할 메시지 문자열의 세 가지 매개 변수가 있다.
이떄 호출하고자 하는 C# 스크립트는 반드시 public void 메소드 명 (string msg)
형태여야 한다.
- 📗 참고 자료
부록
본문에 적기는 사소하지만, 정리해두면 좋을 내용들을 아주 간단하게라도 적어둔다.
🔸 C# | 객체 지향 언어
객체(Class) 중심으로 프로그래밍을 한다는 컴퓨터 프로그래밍 패러다임이다.
- 추상화 : 객체의 공통적인 속성과 기능을 추출하여 정의하는 것
- 상속 : 상위 클래스로부터 하위 클래스를 만들어 코드의 생산성과 유지보수성을 높이는 것
- 다형성 : 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것
- 캡슐화 : 외부로부터 데이터를 보호하거나 은닉하는 것
🔸 C# | 강한 타입 언어
C#은 Java와 함께 대표적인 강한 타입 언어 (Strongly typed language)이다.
강한 타입 언어는 변수의 데이터 타입이 컴파일 시점에 결정되며, 변수가 선언 될 때 명시적으로 데이터 타입을 지정해주어야 한다.
변수가 모든 타입의 값을 가질 수 있도록 설계된 언어는 Weakly type(약한 타입화 언어)이다.
- Python, Javascript, Ruby 등
하지만 C#에선 var
키워드를 통해 특별히 자료형을 지정하지 않아도 알아서 선언되도록 할 수 있다.
단, 이렇게 var
를 사용 할 경우, 반드시 선언과 동시에 초기화를 해 주어야 한다.
🔸 C# | Compiled Language vs Interpreted Language
Compiled Language와 Interpreted Language는 프로그래밍 언어가 실행될 때 처리되는 방식에 차이가 있다.
- 컴파일 언어 : 소스코드를 컴퓨터가 이해할 수 있는 기계어로 변환해 실행파일을 만드는 방식
- 인터프리터 언어 : 소스코드를 바로 실행 하는 방식. 한 줄씩 읽어 파싱하고 컴퓨터가 이해할 수 있는 기계어로 만드는 방식
특징 | Compile | Interpreter | 설명 |
---|---|---|---|
실행 속도 | 빠름 | 느림 | 컴파일러가 소스코드를 기계어로 미리 변환하기 때문에 컴파일 언어가 더 빠름 |
디버깅 | 어려움 | 쉬움 | 인터프리터 언어는 사람이 쓴 코드로 실행하기 때문에, 디버깅이 쉬움 |
이식성 | 낮음 | 높음 | 컴파일 언어는 특정 운영체제나 아키텍처에 맞게 컴파일 해야해서 이식성이 낮음 |
개발속도 | 느림 | 빠름 | 컴파일 언어는 컴파일 시간이나 수정 후 재컴파일등의 시간이 소요되어 개발 속도가 느림 |
가장 큰 특징으로는, 컴파일언어는 빌드 후 실행파일을 얻을 수 있지만, 인터프리터 언어는 저 수준 언어로 된 실행파일이 없다는 것이다.
🔸 C# | Software Framework
소프트웨어 프레임워크란, 애플리케이션 개발을 위한 구조와 기능을 제공하는 환경이다.
일반적으로 라이브러리, 컴파일러, 런타임, 디버깅 도구 등등을 포함하는 개념이다.
- 개발자가 쉽고 빠르게 개발 할 수 있도록 해준다.
- 개발을 위한 표준화된 구조를 제공해 개발자들이 일관성 있고 효율적인 코드를 작성하도록 한다.
🔸 C# | 구조체
구조체는 사용자 정의 데이터 유형(Data Type)으로
int, double 등과 같은 기본적으로 제공되는 변수 유형이 아닌 새로운 유형으로 개발자가 직접 만들어 사용한다.
C#에서, 구조체는 Class와 유사하게 변수 선언, 함수 선언 등이 가능하지만 몇가지 차이점으로 인해 다르게 사용해야한다.
- Struct 는 상속을 할 수 없다. (따라서 protected를 사용할 수 없다.)
- Struct 는 값타입이고, Class 는 참조타입이다.
- 복사 할당시, class는 참조 자체만 복사하지만 구조체는 전체 값 자체를 복사한다.
🔸 C# | COM Marshaller란 무엇일까
일단 COM (Component Object Model)이란 어떤 프로그램이나 시스템을 이루는 컴포넌트들이 상호 통신할 수 있도록 하는 메커니즘이다.
여기서 컴포넌트란 .dll, .exe를 확장자로 갖는 실행 가능한 어셈블리들을 뜻한다.
COM은 마이크로소프트가 제안한 표준이며, COM Marshaller는 이러한 컴포넌트를 네트워크상에서 교환 할 때, 직렬화하는 기능을 담당한다고 한다.
🔸 C# | .NET Framework에서 Unmanaged code를 사용 할 수 없는가
다른 프로그래밍 언어들과 다르게 Visual C++는 Unmanaged 프로그램을 만들수 있다고 한다.
🔸 C# | C#에서 Unmanaged code를 사용 할 수 없는가
자료가 별로 없어서 많이 찾아보진 못하였다.
C#에서 Unmanaged Code, C++코드를 호출하는 방법은 다음 3가지가 있다.
- COM Interop
COM Interop은 CLR에 포함된 컴포넌트 오브젝트 모델(COM) 개체를 상호 운용할 수 있게 만드는 기술이다.
COM Interop은 .NET 환경에서 Native Code를 호출하는 가장 비효율적인 방법이며,
CLR의 객체 생명 주기 관리와 COM 객체의 생명 주기가 달라서 이 점에 대해서도 충분히 이해를 하고 있어야 한다.
- P/Invoke
Platform Invocation Services의 약자로, CLR과 같은 CLI 구현 요소 중 하나이다.
이 방법은 Managed Code를 Native Code로 변경하는 역할을 한다.
P/Invoke로 호출하고자는 함수를 형식에 맞게 다시 선언해야 한다.
이는 Native 코드의 함수를 어떻게 Managed Code에서 사용되어야 함을 정의하는 것이므로 반드시 필요하다.
- 컴파일 옵션 추가
[MSDN] /clr (Common Language Runtime Compilation)
컴파일시 /clr 옵션을 사용하면 Native 힙을 사용하는 MSIL 기반의 라이브러리를 생성할 수 있다.
이 방식의 최대 장점은 Managed Code와 Unmanaged Code 사이를 직접 컨트롤할 수 있어 프로그램 작성중에 문제를 통제할 수 있다는 것이다.
하지만 이는 반대로 C++, C# 기반의 모든 프로그래밍을 해야하는 복잡성을 증대시키는 단점도 가지게 된다.
🔸 C# | C#에서 포인터 사용하기
C# 에서는 일반적으로 포인터를 사용하지 않지만, 간혹 C++ 에서 사용하던 Class를 가져오면서 포인터를 사용해야 하는 경우가 발생한다.
포인터를 사용하면 CLR에서 안전성을 책임지지 못하고, 불완전한 코드가 된다고 하며 에러를 발생하는데,
이렇게 예전 코드를 사용해야 할 경우, 빌드 속성을 변경해줌으로써 unsafe 코드를 사용 할 수 있다.
또한, 클래스가 인스턴스화 되면 CLR에 의해 언제든지 메모리가 이동 될 수 있는데,
fixed
지시어를 이용하면 해당 코드에 들어간 변수 및 객체는 가비지 콜렉팅이 발생할 때 주소 재배치 대상이 아니게 됩니다.
fixed
지시어는 unsafe
컨텍스트에서만 허용 된다.
또한, 빌드시 unsafe
코드를 허용해줘야 문제없이 빌드된다.
🔸 C# | Generic
C# 에서 은 데이터의 타입을 확정하지 않고 이 타입 자체를 타입파라미터(Type Parameter)로 받아들이도록 클래스를 정의하는 기법을 말한다.
C#의 Generic
은 데이터 형식을 매개변수로 받아들여 코드의 재사용성과 유연성을 높이는 기능이다.
Generic
을 사용하면 특정 데이터 형식이 아닌 여러 데이터 형식에서 동일한 코드를 재사용할 수 있으므로,
코드의 중복을 줄이고 유지보수성을 높일 수 있다.
또한, Generic은 컴파일 시점에 형식 검사를 수행하므로 실행 시간에 형식 변환이 필요하지 않다.
따라서 Boxing/Unboxing 과 같은 오버헤드가 발생하지 않아 실행 속도가 빠릅니다.
.NET Framework에는 상당히 많은 제네릭 클래스들이 포함되어 있는데, 특히 System.Collections.Generic 네임스페이스에 있는 모든 자료구조 관련 클래스들은 제네릭 타입이다.
흔히 사용하는 List, Dictionary, LinkedList 등의 클래스들은 이 네임스페이스 안에 들어 있다. 아래는 이들을 사용한 한 예이다.
List<string> nameList = new List<string>();
nameList.Add("철수");
nameList.Add("영희");
Dictionary<string, int> dic = new Dictionary<string, int>();
dic["철수"] = 100;
dic["영희"] = 90;
C# 제네릭 타입을 선언할 때, 타입 파라미터가 Value Type인지 Reference Type인지,
또는 어떤 특정 Base 클래스로부터 파생된 타입인지, 어떤 인터페이스를 구현한 타입인지 등등을 지정할 수 있는데,
이는 where T : 제약조건
과 같은 식으로 where 뒤에 제약 조건을 붙이면 가능하다.
// T는 Value 타입
class MyClass<T> where T : struct {}
// T는 Reference 타입
class MyClass<T> where T : class {}
// T는 디폴트 생성자를 가져야 함
class MyClass<T> where T : new() {}
// T는 MyBase의 파생클래스이어야 함
class MyClass<T> where T : MyBase {}
// T는 IComparable 인터페이스를 가져야 함
class MyClass<T> where T : IComparable {}
// 복수 타입 파라미터 제약
class MyClass<T, U>
where T : class
where U : struct
{
}
🔸 C# | DLL 만드는 방법
DLL을 만들기 위해선, VS에서 클래스 라이브러리를 선택하고 프로젝트를 만들면 된다.
DLL 내에서 사용하는 함수나 클래스등은 모두 외부에서 사용할 수 있어야 하므로, 한정자는 반드시 public으로 지정하여야 한다.
그 후, 솔루션 빌드를 하면 DLL이 생성된다.
만약, UnityEngine
과 같은 namespace의 class를 사용하고싶다면, 해당 dll(UnityEngine.dll
)을 참조로 걸어주어야 한다.
(해당 dll은 Unity Editor가 설치된 위치의 Data/Managed
를 살펴보면 얻일 수 있다.)
🔸 Unity | Nuget이란?
마이크로소프트에서 만든, 마이크로소프트 개발 플랫폼에서 쓰이는 무료 또는 오픈소스의 패키지 관리자이다.
주로 C#과 관련된 패키지가 많으며, 라이브러리들을 솔루션에서 쉽게 가져와 쓸 수 있도록 해주는 도구이다.