2018년 10월 18일 목요일

C# 간단한 4방향 A*(A Star) 알고리즘 구현 - Simple 4-way A* Algorithm implementation in C#

http://gigi.nullneuron.net/gigilabs/a-pathfinding-example-in-c/#comment-51778

위 링크를 따라가보면 c#을 이용해서 동서남북으로 이동하는 A*(A 스타) 알고리즘을 간단히 구현한 소스가 있는데 버그가 있다.

첫째로 G 값을 계산하는 루틴에서 g++ 과 같이 매 스탭마다 무조건 노드의 G 값을 증가시키는데 이렇게 되면 제대로 최단경로를 찾을 수 없다.

두번째로 GetWalkableAdjacentSquares 함수에서 이웃한 노드를 찾을때 Location 을 무조건 새로 생성하는데 만약 openList 에 해당 노드가 존재한다면 openList 에 존재하는 노드의 값을 비교하는것이 아니라 새로 생성한 노드와 비교하기 때문에(모든값이 0) 역시 엉뚱한 경로를 찾게된다.

따라서 위 코드는 아래와 같이 수정되어야한다.

 
 using System;  
 using System.Collections.Generic;  
 using System.Linq;  
 using System.Text;  
 using System.Threading.Tasks;  
   
 namespace AStarPathfinding  
 {  
   class Location  
   {  
     public int X;  
     public int Y;  
     public int F;  
     public int G;  
     public int H;  
     public Location Parent;  
   }  
   
   class Program  
   {  
     static void Main(string[] args)  
     {  
       Console.Title = "A* Pathfinding";  
   
       // draw map  
       string[] map = new string[]  
       {  
                "+------------+",
                "|     X      |",
                "|  X     X  B|",
                "|  X     X   |",
                "|  X     X   |",
                "|A X     X   |",
                "|            |",
                "+------------+",
       };  
       var start = new Location { X = 1, Y = 5 };  
       var target = new Location { X = 12, Y = 2 };  

       int SLEEP_TIME = 100;  
   
       foreach (var line in map)  
         Console.WriteLine(line);  
   
       // algorithm  
       Location current = null;  
       var openList = new List<Location>();  
       var closedList = new List<Location>();  
       int g = 0;  
   
       // start by adding the original position to the open list  
       openList.Add(start);  
   
       while (openList.Count > 0)  
       {  
         // get the square with the lowest F score  
         var lowest = openList.Min(l => l.F);  
         current = openList.First(l => l.F == lowest);  
   
         // add the current square to the closed list  
         closedList.Add(current);  
   
         // show current square on the map  
         Console.SetCursorPosition(current.X, current.Y);  
         Console.Write('.');  
         Console.SetCursorPosition(current.X, current.Y);  
         System.Threading.Thread.Sleep(SLEEP_TIME);  
   
         // remove it from the open list  
         openList.Remove(current);  
   
         // if we added the destination to the closed list, we've found a path  
         if (closedList.FirstOrDefault(l => l.X == target.X && l.Y == target.Y) != null)  
           break;  

         var adjacentSquares = GetWalkableAdjacentSquares(current.X, current.Y, map, openList);  
         g = current.G + 1;  

         foreach(var adjacentSquare in adjacentSquares)  
         {  
           // if this adjacent square is already in the closed list, ignore it  
           if (closedList.FirstOrDefault(l => l.X == adjacentSquare.X  
               && l.Y == adjacentSquare.Y) != null)  
             continue;  
   
           // if it's not in the open list...  
           if (openList.FirstOrDefault(l => l.X == adjacentSquare.X  
               && l.Y == adjacentSquare.Y) == null)  
           {  
             // compute its score, set the parent  
             adjacentSquare.G = g;  
             adjacentSquare.H = ComputeHScore(adjacentSquare.X, adjacentSquare.Y, target.X, target.Y);  
             adjacentSquare.F = adjacentSquare.G + adjacentSquare.H;  
             adjacentSquare.Parent = current;  
   
             // and add it to the open list  
             openList.Insert(0, adjacentSquare);  
           }  
           else  
           {  
             // test if using the current G score makes the adjacent square's F score  
             // lower, if yes update the parent because it means it's a better path  
             if (g + adjacentSquare.H < adjacentSquare.F)  
             {  
               adjacentSquare.G = g;  
               adjacentSquare.F = adjacentSquare.G + adjacentSquare.H;  
               adjacentSquare.Parent = current;  
             }  
           }  
         }  
       }  
   
       Location end = current;  
   
       // assume path was found; let's show it  
       while (current != null)  
       {  
         Console.SetCursorPosition(current.X, current.Y);  
         Console.Write('_');  
         Console.SetCursorPosition(current.X, current.Y);  
         current = current.Parent;  
         System.Threading.Thread.Sleep(SLEEP_TIME);  
       }  
   
       if (end != null)  
       {  
         Console.SetCursorPosition(0, 20);  
         Console.WriteLine("Path : {0}", end.G);  
       }  
   
       // end  
       Console.ReadLine();  
     }  
     
     static List<Location> GetWalkableAdjacentSquares(int x, int y, string[] map, List<Location> openList)  
     {  
       List<Location> list = new List<Location>();  
   
       if (map[y - 1][x] == ' ' || map[y - 1][x] == 'B')  
       {  
         Location node = openList.Find(l => l.X == x && l.Y == y - 1);  
         if (node == null) list.Add(new Location() { X = x, Y = y - 1 });  
         else list.Add(node);  
       }  
   
       if (map[y + 1][x] == ' ' || map[y + 1][x] == 'B')  
       {  
         Location node = openList.Find(l => l.X == x && l.Y == y + 1);  
         if (node == null) list.Add(new Location() { X = x, Y = y + 1 });  
         else list.Add(node);  
       }  
   
       if (map[y][x - 1] == ' ' || map[y][x - 1] == 'B')  
       {  
         Location node = openList.Find(l => l.X == x - 1 && l.Y == y);  
         if (node == null) list.Add(new Location() { X = x - 1, Y = y });  
         else list.Add(node);  
       }  
   
       if (map[y][x + 1] == ' ' || map[y][x + 1] == 'B')  
       {  
         Location node = openList.Find(l => l.X == x + 1 && l.Y == y);  
         if (node == null) list.Add(new Location() { X = x + 1, Y = y });  
         else list.Add(node);  
       }  
   
       return list;  
     }  
   
     static int ComputeHScore(int x, int y, int targetX, int targetY)  
     {  
       return Math.Abs(targetX - x) + Math.Abs(targetY - y);  
     }  
   }  
 }  


위 코드의 결과를 이전 코드와 비교하면 다른것을 확인할 수 있다.

< 이전코드 >



<수정된 코드 >





2018년 9월 4일 화요일

C# 간단한 쓰레드 안전 제네릭 큐 - C# Simple Thread-Safe Generic Queue Source Code


 protected class MyQueue<T>  
     {  
       private int MAX_COUNT = 100;  
       private Queue<T> queue = new Queue<T>();  
   
       public MyQueue(int count)  
       {  
         MAX_COUNT = count;  
       }  
   
       public bool Push(T info)  
       {  
         lock (queue)  
         {  
           if (MAX_COUNT == 0 || queue.Count < MAX_COUNT)  
           {  
             queue.Enqueue(info);  
             return true;  
           }  
           return false;  
         }  
       }  
   
       public T Pop()  
       {  
         lock (queue)  
         {  
           if (queue.Count > 0) return queue.Dequeue();  
           return default(T);  
         }  
       }  
   
       public void Clear()  
       {  
         lock (queue)  
         {  
           queue.Clear();  
         }  
       }  
   
       public int Count { get => return queue.Count; }  
     }  

2018년 6월 1일 금요일

Jenkins workspace 변경

Jenkins 설치 디렉토리 (C:\Program Files (x86)\Jenkins) 아래에 config.xml 에
아래와 같이 변경 후 재시작

<workspaceDir>[workspace 경로]/${ITEM_FULL_NAME}</workspaceDir>

2017년 11월 17일 금요일

c# 윈폼 사용시 주의할 점 - 메모리릭 방지

c#에서 간단하게 모달 다이얼로그 폼을 띄울때 보통 아래와 같이 한다.

MyForm form = new MyForm();
if (form.ShowDialog() == DialogResult.OK)
{
    ...
}

위 코드는 별 문제 없어보이지만 사실은 메모리릭이 발생하는 코드이다.
폼 클래스는 IDisposable 을 구현하기 때문에 자원을 해제하려면 반드시 Dispose() 를
호출해줘야한다.
폼 내부에서 Dispose()를 호출하는 방법도 있지만 아래와같이 using 으로 묶어주면 깔끔하다.

using (MyForm form = new MyForm())
{
    if (form.ShowDialog() == DialogResult.OK)
    {
        ...
    } 
}

MyForm 객체를 멤버변수로 선언해서 재사용해서 쓰는것도 방법이겠지만 간단한 다이얼로그를 멤버변수로 두면 코드가 불필요하게 복잡해진다. 따라서 간단하게 using 으로 묶어주는것이 좋다.

위와같이 Dispose 를 해주지않고 반복해서 폼을 생성하면 "Win32Exception - error creating window handle" (윈도우 핸들을 생성할 수 없습니다) 와 같은 예외를 만나게된다.

2017년 10월 5일 목요일

ffmpeg+x264 윈도우/안드로이드 빌드(32/64비트)

ffmpeg을 빌드하는 방법 중 가장 확실하고 안정적인 방법은 리눅스(우분투)에서 빌드하는 것이다. 윈도우에서 mingw+msys 조합으로 빌드하는 방법도 있는데 여러모로 우분투에서 하는것이 정신건강에 좋다.
우분투는 실제 PC에 설치하지않고 VirtualBox 에 설치하면 간편하게 쓸수있다.

리눅스에서 mingw 설치

리눅스에서 아래 경로의 git 소스를 clone 받는다.

https://github.com/Zeranoe/mingw-w64-build.git

./mingw-w64-build 실행

위 스크립트를 실행하면 필요한 툴과 mingw 소스코드를 다운받은 다음 gcc로 빌드해서 윈도우용 32/64비트 크로스컴파일 툴채인을 만들어준다.
i686-w64-mingw32 아래에 32비트
x86_64-w64-mingw32 아래에 64비트 빌드 툴채인이 만들어진다.

따라서 쉘 환경의 경로설정에 따라 32/64비트 빌드 환경을 결정할 수 있다.

export PATH=$PATH:/home/ubuntu/work/mingw/mingw-w64-build/i686-w64-mingw32/bin => 32비트

export PATH=$PATH:/home/ubuntu/work/mingw/mingw-w64-build/x86_64-w64-mingw32/bin => 64비트

x264 빌드

./configure --cross-prefix=i686-w64-mingw32- --host=i686-w64-mingw32 => 32비트

./configure --cross-prefix=x86_64-w64-mingw32- --host=x86_64-w64-mingw32 => 64비트

make

빌드에 성공하면 libx264.a 파일이 생성된다.

ffmpeg 빌드

위에서 빌드한 libx264.a 파일을 정적으로 링크해서 빌드해주면된다.

※ x264 소스코드 : /home/ubuntu/work/mingw/x264
   ffmpeg 소스코드 : /home/ubuntu/work/mingw/ffmpeg
   output : /home/ubuntu/work/mingw/output

32비트
./configure --arch=x86 --target-os=mingw32 --cross-prefix=i686-w64-mingw32- --pkg-config=pkg-config --enable-w32threads --prefix=../output/ffmpeg_x86/ --enable-shared --disable-static --enable-gpl --enable-libx264 --extra-cflags=-I/home/ubuntu/work/mingw/x264 --extra-ldflags=-L/home/ubuntu/work/mingw/x264

64비트
./configure --arch=x86_64 --target-os=mingw32 --cross-prefix=x86_64-w64-mingw32- --pkg-config=pkg-config --enable-w32threads --prefix=../output/ffmpeg_x64/ --enable-shared --disable-static --enable-gpl --enable-libx264 --extra-cflags=-I/home/ubuntu/work/mingw/x264 --extra-ldflags=-L/home/ubuntu/work/mingw/x264

make
make install 하면 /home/ubuntu/work/mingw/output/ 아래에 빌드파일들 복사됨


안드로이드 NDK 빌드

안드로이드 NDK 를 다운받은 다음 툴채인을 만든다. 툴채인을 꼭 만들필요는 없는데 안만드는 경우 해당 아키텍쳐의 빌드경로만 설정해주면 된다. 여기서는 툴채인을 만들도록 하겠다.(툴채인 만드는법은 다른 포스트 참조 - http://greenday96.blogspot.kr/2015/01/android-ffmpeg-build.html)

툴채인 경로 : /home/ubuntu/work/my-android-toolchain-18

x264 빌드

./configure --cross-prefix=/home/ubuntu/work/my-android-toolchain-18/bin/arm-linux-androideabi- --host=arm-linux --extra-cflags='-marm -march=armv7-a -mfloat-abi=softfp -mfpu=neon -D__ANDROID_API__=18' --extra-ldflags='-Wl,--fix-cortex-a8' --enable-pic

make
빌드를 하면 안드로이드용 libx264.a 라이브러리가 생성된다.


ffmpeg 빌드

./configure --target-os=linux --arch=arm --enable-cross-compile --cc=/home/ubuntu/work/my-android-toolchain-18/bin/arm-linux-androideabi-gcc --cross-prefix=/home/ubuntu/work/my-android-toolchain-18/bin/arm-linux-androideabi- --prefix=../output/android --extra-cflags='-marm -march=armv7-a -mfloat-abi=softfp -mfpu=neon -D__ANDROID_API__=18' --extra-ldflags='-Wl,--fix-cortex-a8 -llog' --enable-shared --enable-gpl --enable-libx264 --extra-cflags=-I/home/ubuntu/work/mingw/x264 --extra-ldflags=-L/home/ubuntu/work/mingw/x264

make
make install
하면 /home/ubuntu/work/mingw/output/android 아래에 빌드된 파일들이 복사된다

Android.mk 파일

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := avcodec
LOCAL_SRC_FILES := $(LOCAL_PATH)/libs/libavcodec.a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avformat
LOCAL_SRC_FILES := $(LOCAL_PATH)/libs/libavformat.a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swscale
LOCAL_SRC_FILES := $(LOCAL_PATH)/libs/libswscale.a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avutil
LOCAL_SRC_FILES := $(LOCAL_PATH)/libs/libavutil.a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swresample
LOCAL_SRC_FILES := $(LOCAL_PATH)/libs/libswresample.a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avfilter
LOCAL_SRC_FILES := $(LOCAL_PATH)/libs/libavfilter.a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := postproc
LOCAL_SRC_FILES := $(LOCAL_PATH)/libs/libpostproc.a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := x264
LOCAL_SRC_FILES := $(LOCAL_PATH)/libs/libx264.a
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)

LOCAL_MODULE    := DXMediaPlayer
LOCAL_SRC_FILES := DXMediaPlayer.cpp \
   DXMediaPlayerCtrl.cpp

LOCAL_LDLIBS := -llog -lz -ljnigraphics -landroid -Wl,--no-warn-shared-textrel

LOCAL_STATIC_LIBRARIES := avfilter avformat avcodec avutil swscale swresample x264 postproc

LOCAL_CFLAGS := -DANDROID -D__STDC_CONSTANT_MACROS
LOCAL_CPPFLAGS := -DANDROID -D__STDC_CONSTANT_MACROS

include $(BUILD_SHARED_LIBRARY)



콘솔창에서 빌드

ndk-build

빌드에 성공하면 x264+ffmpeg 포함된 so 파일 생성








2017년 9월 28일 목요일

ActiveX <-> C# 포인터 매개변수 전달 - Passing pointer parameters between activex and c#

vc++로 작성된 activex 와 c#으로 작성된 호스트 프로그램 간에 포인터 값을 전달해야하는 경우는

1. c# -> activex 메소드 호출
2. activex -> c# 이벤트 호출

두 가지 경우가 있다. activex가 c#과 연동하기 위해서는 activex 에서 적절한 메소드를 만든다음 커맨드창에서

aximp /source MyActiveX.ocx

해서 만들어진 인터페이스용 cs 소스파일과 dll 을 사용한다.
이때 메소드의 매개변수를 c++의 BYTE* 와같이 포인터로해도 c#에서는 포인터를 매개변수로 사용할 수 없으므로 만들어진 cs 파일에는 ref byte 타입으로 변경되어 포인터값을 전달할 수 없다.
2의 경우도 마찬가지로 aximp 커맨드를 통해 생성되는 인터페이스용 cs 파일에는 포인터 타입이 들어가지않는다.

activex가 아닌 dll 인 경우 c# 함수 프로토타입 선언에서 byte[] 타입으로 매개변수를 지정해서 쓰면되지만 activex(ocx)는 aximp 로 생성된 소스를 사용해야하므로 난감한 상황이 발생한다.

결론적으로 포인터는 결국 32(혹은 64)비트 정수값일 뿐이며 사용하기에 따라 BYTE*, LONG* 등으로 캐스팅해서 사용할 뿐이다. (결국 해석의 차이)
따라서 매개변수를 굳이 포인터 타입으로 할 이유가 없다. activex 쪽에서 LONGLONG 타입(64비트 프로세스인 경우 대비해서 LONG보다 LONGLONG 권장)으로 매개변수를 지정한다음 포인터값(주소)을 넘겨주거나 넘겨받으면 그만이다.

1. 포인터 매개변수 전달 예 - activex

void MyActiveXCtrl::SetIntArrayData(LONGLONG pData)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());

// TODO: Add your dispatch handler code here
        int *data = (int *)pData;
        ...
}

aximp /source MyActiveX.ocx => cs 파일 생성

1. 포인터 매개변수 전달 예 - c#

int[] data = new int[10];
for (int i = 0; i < data.Length; i++) data[i] = i;

IntPtr ptr = Marshal.AllocHGlobal(sizeof(int) * data.Length);
Marshal.Copy(data, 0, ptr, data.Length);

myAxCtrl.SetIntArrayData((long)ptr); => c# long은 vc++ LONGLONG 이므로

Marshal.FreeHGlobal(ptr);


2. 포인터 매개변수 전달 예 - activex

void MyActiveXCtrl::OnDataEvent(LONG size, LONGLONG pData)
{
    FireEvent(eventidOnDataEvent, ...., pData);
}


2. 포인터 매개변수 전달 예 - c#

myAxCtrl.OnDataEvent += axAxCtrl_OnDataEvent;

private void axAxCtrl_OnDataEvent(_AXMyActiveXCtrlEvents_OnDataEvent e)
{
    byte[] btaImage = new byte[e.size];
    unsafe
    {
        byte* p = (byte*)e.pData;
        Marshal.Copy((IntPtr)p, btaImage, 0, e.size);
    }
    // now you can use btaImage data
    ...
}


1번의 경우 c# -> activex 로 포인터를 전달하는데 원하는 데이터(여기서는 int 배열)를 IntPtr 변수에 복사한다음 포인터값을 전달한다. 이때 포인터를 받는 c++에서는 LONGLONG 타입을 int* 로 타입캐스팅해서 사용하면된다.

2번의 경우 activex -> c# 으로 포인터를 전달하는데 포인터 size를 지정한다음 LONGLONG 타입(64비트) 정수에 포인터 주소값을 넣어서 c# 쪽으로 전달한다.
c#에서는 전달받은 long 타입(64비트) 데이터를 byte* 으로 캐스팅한다음 c#에서 쉽게 사용하도록 byte 배열에 복사한다.


2017년 8월 24일 목요일

C# 쓰레드 올바른 사용법

C# Thread 관련 API 중 왠만하면 절대 사용하면 안되는 함수

Thread.Abort()
Thread.Interrupt()

위 두 함수는 극단적인 상황이 아니면 절대 사용해서는 안된다. 대신 모든 쓰레드의 종료는
Thread.Join() 함수로 정상종료를 확인해야한다.

쓰레드는 자체 메모리 공간을 가지고있고 프로세스의 전역변수를 사용할 수 있다.
Abort/Interrupt 함수는 해당 쓰레드가 어떤 동작을 하는중인지 상관없이 CPU 사용권을 빼앗아 강제종료 시키기때문에 함수 호출시 어떤일이 발생할지 알 수가 없다.
예를 들어 쓰레드가 전역변수 뮤텍스를 사용중인데 강제종료 되었다면 해당 뮤텍스를 사용하는 다른 쓰레드는 데드락에 빠져버린다.
굳이 이런 상황이 아니더라도 쓰레드는 반드시 정상종료 시켜야 프로그램의 완성도를 높이고 오동작을 방지할 수 있다.

쓰레드 시작과 종료는 보통 아래코드와 같이하면 문제가 없다.

     private Thread threadDoWork;  
     private bool bThreadDoWorkRun = false;     
   
     private void StartDoWork()  
     {  
       if (!bThreadDoWorkRun)  
       {  
         bThreadDoWorkRun= true;  
         threadDoWork = new Thread(new ThreadStart(ThreadDoWork));  
         threadDoWork.Start();  
       }  
     }  
   
     private void StopDoWork()  
     {  
       if (bThreadDoWorkRun)  
       {  
         bThreadDoWorkRun = false;  
         threadDoWork.Join();  
       }  
     }  
   
     private void ThreadDoWork()  
     {  
       while (bThreadDoWorkRun)  
       {          
         ...  
       }  
     }  
   

추가적으로 Suspend/Resume 같은 함수도 사용을 권장하지않는다.
두 함수 역시 해당 쓰레드의 동작여부에 상관없이 중지/재시작을 하기때문에 어떤 상황이 발생할지 알 수가 없다.