Trying out Qt

ปกติเวลาลองของใหม่ๆ จะลงในเฟสอยู่เรื่อยๆ แต่จะตั้ง friend only ไว้เพราะว่าเดี๋ยวนี้วงการนี้ดราม่าเยอะ ถ้าจะกด public หรือลง blog ต้องมานั่ง fact check พอสมควรก่อนถึงจะออกได้

ปีนี้เนื่องจาก Facebook ปรับ UI ใหม่ให้น่ารำคาญขึ้น ก็คิดว่าน่าจะถึงเวลาเลิกแล้วกลับมาเขียนในบล็อค ก็เลยคิดว่าจะลองดูใหม่คือเอา content แนวเดิม formatting แย่ๆ แบบเดิมแต่เขียนลงบล็อคดูบ้าง

สำหรับคนที่อาจจะไม่เคยได้อ่าน content พวกนั้น, ปกติจะ:

  • เป็น first impression มากๆ คือไม่ได้ลึกและบางทีที่เชียร์ผ่านไปอีกสองวันก็ไม่เชียร์แล้ว แต่ไม่ได้กลับมาอัพเดตนะ
  • rapid fire จัดๆ เน้น bullet point คิดอะไรออก brain dump ลงไป
  • เขียนแบบคิดว่าคนอ่านเข้าใจใน topic นั้นแล้ว
  • ไม่ได้ proofread

ช่วงนี้โคลนแอพ Alt1 มารันใน Linux อยู่ (Alt1 เป็นโปรแกรมช่วยเล่นเกม RuneScape โดยมันจะ stream หน้าจอให้ web page แล้ว web page ทำ OCR) ก็พบว่าถึงโค้ดมันจะค่อนข้างแย่ แต่ไอเดียรอบๆ มันฉลาดดีแล้วทำให้ port ง่ายมาก เพราะ OCR ทั้งหมดไปรันใน JavaScript หมด

Goal คิดว่าอยากได้ Linux + Mac ก็เลยเลือก Qt ก่อนหน้านี้เคยใช้ Gtk กับ wxWidget มาแล้ว คิดว่า Gtk ไม่น่าจะรันในแมคได้ดี ส่วน wxWidget คิดว่า API จะไม่พอใช้

ปัญหาแรกที่เจอคือคนเขียน cefpython ไม่ได้อัพเดตต่อ คือไม่มีตังให้ก็ไม่ทำ ซึ่งก็ถูกของเค้าล่ะ ก็เลยต้องเปลี่ยน engine ไปใช้ QtWebEngine ซึ่งมันไม่ใช่ CEF แต่มันเป็น Embedded Chromium เหมือนกัน

พอเอา QtWebEngine มารันได้ ถัดมาที่ต้องทำคือจำลอง Alt1 API ซึ่งบน Alt1 มันจะมี window.alt1 ที่มีเมธอดต่างๆ เช่น getRegion เพื่อที่จะ transfer capture มายัง JavaScript ปัญหาก็คือ CEF3 ไม่มีท่าที่สามารถ provide native object ให้ web ได้แล้ว รวมถึง QtWebEngine ด้วย เหตุผลเพราะว่า Blink มันรันเป็น multiprocess แล้วทำให้ไม่สามารถแชร์ของข้าม process ไปได้ ก็จะมีแต่ Async API ให้เท่านั้น

ยังสงสัยอยู่ว่า Alt1 จะทำยังไง หรือจะไม่อัพเกรด CEF?

ก็นึกอยู่คืนหนึ่งว่าทำยังไงดี QtWebEngine ก็ไม่มีแนะนำ จนกระทั่งไปอ่าน docs ของ CEF แล้วมันเขียนว่าให้ทำ synchronous XHR ซึ่งทีแรกก็ลังเลเพราะมันจะ deprecate แล้ว แต่ก็ไม่มีทางเลือกเลยต้องทำ

พอ Get รูปได้รอบแรกดีใจมาก แต่ปัญหาคือพอไปลองกับ app จริงๆ แล้วมัน poll เอารูปค่อนข้างถี่มากๆ แล้ว encoder algorithm มันเขียนด้วย Python ซึ่งช้ามาก (ฝั่ง JS expect รูปเป็นแบบ color channel BGRA ซึ่งต้องสลับเอง) เลยลองไปเขียน Cython ดู พบว่าเขียนง่ายมากๆ และออกเป็น C จริงๆ มีให้ดูด้วย แถมเร็วมากๆ

(ตอนหลังมาพบว่ามัน split() แล้ว merge() เพื่อเรียง channel ใหม่ได้ เร็วกว่า cython ที่เขียนอีก…)

พอรันได้แล้วก็เลยลองหาวิธีต่อกับเกมจริงๆ บ้าง ตอน capture เกมพบว่า Qt สามารถเอา WId ให้มันแล้วมันจะ capture หน้าต่างได้ ซึ่งบน Linux ต้องใช้ Xlib หา X Window ID ออกมาแล้วส่งให้ ที่ Qt ทำคือมันจะแคปจอทั้งหมด แล้ว crop ให้เหลือแต่ด้านในของ Window นั้น ซึ่งก็ง่ายดีแต่แปลว่าห้ามมีอะไรบังหน้าต่าง

ไปต่อ Xlib ตรงๆ นี่ปวดหัวมาก ทำ segfault ไปหลายรอบ ยังขี้เกียจบ่นอยู่เพราะเพิ่งมารู้ว่าควรจะต่อกับ XCB แทน เดี๋ยวจะไปเขียนใหม่


ทีนี้ที่รู้สึกเจ๋งมากคือลอง port ไปรันใน Mac ดูบ้าง ส่วนที่ต้องทำก็คือ

  • หาวิธี list game window บน Mac
  • บน Mac WId คือ Window pointer ของ Quartz ซึ่งไม่มี representation ใน Python แปลว่าใช้ท่า capture เดิมไม่ได้เลย

ก็ลองเขียนต่อกับ Quartz ดูแล้วพบว่า API ของ Mac ใช้งานง่ายกว่าเยอะ docs คุณภาพดีกว่ามาก (ของ Python นี่ไม่มี API docs ต้องนั่งแกะซอร์สเทียบกับ protocol definition) งมอยู่ประมาณ 4 ชั่วโมงก็ใช้ได้ทั้ง 2 feature แถม capture window ที่โดนบังได้ด้วยเพราะ Mac มี API ที่ระบุหน้าต่างแล้วมันเอามาให้ได้เลย

หลังจากใช้งานพื้นฐานได้ ก็ hook กับ Accessibility API อีกหลายชั่วโมงก็สามารถจับ game resize, game activity (ถ้าไม่กดเมาส์นานๆ เกมจะ auto logout) และ hotkey ได้ เป็นอันสมบูรณ์ก่อน Linux

แต่ว่า port Mac จริงๆ ก็ยังมีปัญหาอยู่บ้าง นั่นคือ

  1. Capture API ใน Mac คืนกรอบหน้าต่างมาด้วย ถึงจะบอกว่าไม่เอามันจะไม่ให้เงามาแต่ยังให้กรอบอยู่ดี ส่งผลให้ API ที่ใช้ตำแหน่งต่างๆ เลื่อนหมด
  2. Mac ใช้ HiDPI (Retina display) เป็นหลักและมันทำให้พิกัดต่างๆ ปวดหัวมากว่าอันนี้เป็น screen size หรือ actual size และตัวเกมเองไม่ได้ support Retina ด้วย

ทีรู้สึกว่า Qt มันเจ๋งมากๆ คือด้านบนที่เล่ามามีแค่ port ส่วนที่ต่อกับเกมซึ่ง Qt ไม่มี API ที่ไปเชื่อมต่อกับหน้าต่างแอพอื่นๆ สักเท่าไร แต่ในส่วน browser แล้วใช้โค้ดเดียวกันรันได้ทุก platform เหมือนกันเป๊ะ คือ Qt abstract platform ให้ได้ดีมากๆ บวกกับ Python ด้วย

เดี๋ยวถึงตอนจะทำ embed window, custom decoration ค่อยมาดูว่าจะรอดมั้ย

Go look at my code >> https://github.com/whs/runekit


สุดท้ายซ่าอยากลอง เลยลอง Port ไปรันใน Windows ดูบ้าง

  • WId บน Windows ใช้ HWND ของ Windows ได้เลย
  • ฟังค์ชั่น capture ของ Qt รันบน HWND แล้วได้ window capture !! สุดยอดมาก
  • API Windows เท่าที่ดูยังงงๆ ว่าอะไรเรียกว่าอะไร แต่ถ้าเจอแล้วก็เรียกใช้ได้ง่ายกว่า Xlib บน Linux อยู่ดี…
Real Alt1 vs clone

เขียน project นี้แล้วก็เริ่มรู้สึกเหมือนตอนเด็กกว่านี้ว่าต่อไปนี้เราทำ Qt UI ดีกว่า เขียนทีเดียวรันได้ทุกที่ แถม native ไม่ต้องใช้แล้ว React Native หรือ Flutter

แต่ปัญหาใหญ่ของ Qt คือมันเป็นบริษัทเขียนมันถึงจะออกมาได้ดีขนาดนี้ และบริษัทเองก็ให้ใช้งานภายใต้ LGPL ฟรี ปัญหาคือพอเป็น mobile app แล้ว โดยเฉพาะ iOS จะค่อนข้างยากที่จะทำให้ user สามารถ replace LGPL library เองได้ ถ้าจะซื้อรุ่นเสียเงินก็ราคาไม่ได้ถูกเลย โดยเฉพาะเมื่อเทียบกับ React Native หรือ Flutter ที่มันใช้ฟรี

แต่ทั้งสอง project เอง ก็ยังรู้สึกว่าเป็น corporate back อยู่ประมาณหนึ่งไม่ได้มี community มาทำแทนกันได้หมด ถึงจะมีคนใช้มากแต่ถ้า sponsor หลักไม่ทำก็อาจจะ innovate ไม่ทันเทคโนโลยีอื่นได้ ก็เป็นปัญหา classic ของ open source ที่ corporate ใช้ว่า take อย่างเดียวแต่ไม่ give

Wall of Text #5: Internet Scale List Comprehension

ในทีมผมมีคนแนะนำ Codewars มา ผมก็เลยเอาไปปล่อยต่อให้น้องเค้าเล่น ซึ่งมันสนุกมาก

เวลาทำโจทย์ใน Codewars ผ่าน มันจะให้ดูว่าคนอื่นแก้โจทย์นี้อย่างไร ซึ่งถ้าเป็น Python เรามักจะเห็นเฉลยระดับบรรทัดเดียวออก เช่น

def find_it(seq):
    return [i for i in seq if seq.count(i) % 2 != 0]

โค้ดนี้เรียกว่า List Comprehension ซึ่งสำหรับมือใหม่อาจจะดูซับซ้อน ผมเองก็เขียน Python มาประมาณ 3-4 ปีก่อนที่จะเริ่มใช้เป็น

List Comprehension ประกอบด้วยคอนเซปต์จาก Functional Programming 2 ตัว คือ Map และ Filter

Map

Map คือการเปลี่ยนทุกสมาชิกของ List ให้เป็นค่าใหม่ โดยเรียกฟังค์ชั่นที่กำหนดไว้กับสมาชิกทุกตัว แล้วเอาคำตอบที่ได้แทนค่าเดิม ดังนั้นผลลัพท์ของ Map จะมีจำนวนสมาชิกเท่ากับค่าตั้งต้นเสมอ

ยกตัวอย่างเช่น ถ้ากำหนดฟังค์ชั่นคือ ยกกำลังสอง แล้ว Map function นี้ลงบน [1, 2, 3, 4] ผลลัพท์ที่ได้คือ [1, 4, 9, 16]

Filter

Filter แปลตรงตัวก็แปลว่ากรอง โดยจะเอาฟังค์ชั่นที่คืนค่าเป็น Boolean รันบนสมาชิกทุกตัวของ List เพื่อถามว่ายังต้องการเก็บสมาชิกตัวนี้ไว้ไหม ผลลัพท์ของ Filter จะมีจำนวนสมาชิกไม่เกินจำนวนตั้งต้นเสมอ และข้อมูลใน List จะไม่ถูกแก้ไข เอาสมาชิกออกได้อย่างเดียว

ยกตัวอย่างเช่น ถ้ากำหนดฟังค์ชั่นคือ ทดสอบว่าเป็นเลขคู่ แล้ว Map ลงบน [1, 2, 3, 4, 5] ผลลัพท์ที่เหลืออยู่คือ [2, 4]

ในหลายๆ ภาษา กระบวนการ Map กับ Filter นี้จะแยกกันเป็นสองคำสั่ง เช่นในภาษา JavaScript [1,2,3].map((x) => x*2).filter((x) => x<5) แต่ใน Python เราสามารถรวบเป็น List Comprehension ครั้งเดียวได้เพื่อความสะดวก

Reduce

โดยปกติแล้วฟังค์ชั่นตระกูลนี้นอกจาก Map – Filter แล้วยังมักจะมี Reduce ด้วย (ซึ่ง List Comprehension ทำไม่ได้)

Reduce คือการนำสมาชิก 2 ตัวเข้ามารวมกันเหลือค่าเดียว เช่น ถ้าหากต้องการหาผลรวมของ [1, 2, 5] เราสามารถใช้ reduce function คือ x+y แล้วระบบจะรันฟังค์ชั่นนี้จนครบทุกสมาชิกให้เอง คือ

  • x=1, y = 2; 1+2 = 3
  • x=3, y = 5; 3+5 = 8
  • คำตอบสุดท้ายคือ 8

คำสั่ง Reduce ใน Python 2 คือ reduce() และ Python 3 ถูกย้ายไปไว้ใน functools.reduce() ด้วยเหตุผลว่าเราสามารถใช้ For loop แทนกันได้และอ่านเข้าใจง่ายกว่า

MapReduce

กระบวนการ Map – Reduce – Filter นี้ทรงพลังมาก ขนาด Google ที่มีข้อมูลมหาศาลก็ยังใช้กระบวนการนี้ประมวลผลข้อมูลบางอย่างอยู่

สาเหตุที่มันทรงพลังมาก ก็เพราะมันทำให้ปัญหานี้สามารถกระจายงานไปหลายๆ เครื่องพร้อมกันได้

ยกตัวอย่างเช่น ถ้าหากเราต้องการทำโปรแกรมนับคะแนนจากภาพบัตรเลือกตั้ง วิธีก็คือ

  1. Map – แปลงภาพบัตรเลือกตั้งเป็นพรรคที่เลือก
  2. Filter – กรองบัตรเสียออก
  3. Reduce – รวมคะแนนเลือกตั้งจาก Map

วิธีนี้จะสังเกตว่าในขั้นตอน Map-Filter ไม่มีใครต้องรอใคร ทำให้เรากระจายงานได้ง่ายมาก

ทั้งนี้ MapReduce ที่ใช้ในงาน BigData จะแตกต่างกับ Map-Filter-Reduce อยู่เล็กน้อยเพราะ Map จะมีหน้าที่ของ Filter ไปด้วย โดยในแต่ละ Input Map อาจจะคืนค่ามากกว่า 1 ก็ได้ หรือไม่คืนค่าเลยก็ได้ (แปลว่า filter ทิ้ง) มันจึงจะหน้าตาประมาณนี้

def map_ballot(ballot_img):
    result = ocr(ballot_img)
    if result is not None:
        emit({result: 1})

จากโค้ดด้านบน เราสามารถเรียก emit หลายๆ ครั้งได้ถ้าหากต้องการ output หลายๆ ค่า หรือถ้า result เป็น None จะไม่มีการเรียก emit ก็คือ input นี้จะถูก filter ทิ้งนั่นเอง