Pular para o conteúdo principal

Thiago Lages de Alencar

No post anterior vimos como fazer authenticação com usuário e senha.
Agora vamos ver com email e senha, basicamente igual ao outro porém email é o identificador da conta.

Precisaremos de um servidor de email para receber email:

No lado do server utilizaremos:

No lado do client utilizaremos:

  • httpx para fazer requisições

Questions

Qual a vantagem de ter o email como identificador?

Uma segunda maneira de confirmar autenticação.

Para alguém acessar a conta, ela tem que provar que sabe a senha (vulgo botar a senha na hora de logar).
Agora podemos cobrar que ela prove ser a dona da email (mandamos uma mensagem para o email dela e ela diz para a gente o conteúdo do email).

Em outras palavras, se uma pessoa tem a senha e tem acesso ao email que criou a conta, então ela é a dona da conta.

Como que falar o conteúdo do email vai provar algo?

Vamos dizer que a pessoa pede para mudar a senha da conta.

  • Nós mandamos um email com um código especial e aleatório para ao email dela
  • Ela acessa o email e pega o código (por exemplo: 982jd8fsj83)
  • A pessoa manda o código para o site e a senha nova
  • Nós conferimos se o código que a pessoa nos mandou era o que esperavamos, se for mudamos a senha

Essa é apenas uma maneira de implementar isto, outra maneira mais conhecida é:

  • Enviamos um email com uma URL do site gerada aleatóriamente
  • Usuário acessa o URL
  • Levamos o usuário para a área de trocar a senha
    • Pois a pessoa provou que ela entrou no email e conseguiu o URL único
    • Nesse caso podemos também fazer verificação se ela estava logada na conta que esperava esse URL

Iremos pela primeira maneira.

O que ganhamos com isso?

O famoso botão "recupere sua senha".

Email server

Para não termos que configurar o seu provedor de email ou criar um código onde você passa credenciais reais do seu provedor, vamos levantar um na própria máquina.

Script para preparar o diretório do provedor de email:

mkdir email_server
cd email_server
python3 -m venv venv
. venv/bin/activate
pip install aiosmtpd

Para ligar o provedor de email:

python -m aiosmtpd -n

Server

Script para preparar o diretório dos exemplos:

mkdir server
cd server
python3 -m venv venv
. venv/bin/activate
pip install duckdb starlette uvicorn pytz
touch main.py auth.py database.py

Server - Database Operations

Alteraremos as operações do post anterior para usarem email em vez de usuário.

E adicionaremos 4 operações ao banco no nosso código:

  • Criar código de recuperação de conta
  • Validar o código de recuperação
    • Isso inclui ver se o usuário está passando o código dentro de um tempo limite
  • Remover código de recuperação
  • Alterar senha
database.py
from datetime import datetime, timedelta

import duckdb


def setup():
cursor = duckdb.connect("users.db")

cursor.execute("""
CREATE SEQUENCE IF NOT EXISTS user_id_sequence START 1
""")

cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id integer primary key default nextval('user_id_sequence'),
email text not null unique,
salt text not null,
hash text not null
)
""")

cursor.execute("""
CREATE TABLE IF NOT EXISTS recovery_codes (
email text primary key not null,
code text not null,
time timestamp
)
""")


def get_user_auth(email: str) -> tuple[str, str]:
cursor = duckdb.connect("users.db")
result = cursor.execute(
"SELECT salt, hash FROM users WHERE email = $email",
{"email": email},
).fetchone()

if result:
return (result[0], result[1])
return ("", "")


def create_user(email: str, salt: str, hash: str) -> bool:
cursor = duckdb.connect("users.db")
cursor.execute(
"""
INSERT INTO users (email, salt, hash) VALUES
($email, $salt, $hash)
""",
{"email": email, "salt": salt, "hash": hash},
)


def create_recovery_code(email: str, code: str):
cursor = duckdb.connect("users.db")
cursor.execute(
"""
INSERT INTO recovery_codes (email, code, time) VALUES
($email, $code, current_timestamp)
ON CONFLICT (email) DO UPDATE
SET code = EXCLUDED.code
""",
{"email": email, "code": code},
)


def is_recovery_code_valid(email: str, code: str) -> bool:
cursor = duckdb.connect("users.db")
result = cursor.execute(
"""
SELECT email, code, time, current_timestamp
FROM recovery_codes
WHERE email = $email AND code = $code
""",
{"email": email, "code": code},
).fetchone()

if not result:
return False

created: datetime = result[2]
now: datetime = result[3]
now = now.replace(tzinfo=None)
diff: timedelta = created - now

# Válido por uma hora
if diff.total_seconds() > 3600:
return False

return True


def delete_recovery_code(email: str):
cursor = duckdb.connect("users.db")
cursor.execute(
"""
DELETE FROM recovery_codes WHERE email = $email
""",
{"email": email},
)


def change_account(email: str, salt: str, hash: str):
cursor = duckdb.connect("users.db")
cursor.execute(
"""
UPDATE users
SET salt = $salt,
hash = $hash
WHERE email = $email
""",
{"email": email, "salt": salt, "hash": hash},
)

Server - Authentication

Igual post anterior porém trocamos usuário por email.

auth.py
import base64
import hashlib
import binascii

from starlette.authentication import (
AuthenticationBackend,
AuthenticationError,
AuthCredentials,
SimpleUser,
)
from starlette.requests import HTTPConnection

from database import get_user_auth


class AuthBackend(AuthenticationBackend):
async def authenticate(self, conn: HTTPConnection):
# No authroization, no access
if "Authorization" not in conn.headers:
return

auth = conn.headers["Authorization"]
scheme, credentials = auth.split()

# Wrong authorization scheme
if scheme.lower() != "basic":
return

# Extract email and password from credentials
credentials = base64.b64decode(credentials)
credentials = credentials.decode()
email, _, password = credentials.partition(":")

# Get real hash from database
salt, hash = get_user_auth(email)
if not salt or not hash:
raise AuthenticationError("Invalid credentails")

# Generate guess of hash
password = password.encode()
salt = binascii.a2b_hex(salt)
guess = hashlib.scrypt(password, salt=salt, n=2, r=64, p=1)
guess = binascii.b2a_hex(guess)
guess = guess.decode()

# Check if the hash guess is the same as real hash
if hash != guess:
raise AuthenticationError("Wrong password")

return AuthCredentials(["authenticated"]), SimpleUser(email)

Server - Main

Trocamos usuário por email e adicionamos dois 2 endpoints novos:

  • Pedir o código de recuperação
  • Mudar a senha da conta
main.py
import os
import hashlib
import binascii
import secrets
import smtplib
from urllib.parse import parse_qs
from email.message import EmailMessage

from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.requests import Request
from starlette.routing import Route
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.authentication import requires

import database
from auth import AuthBackend


async def register(request: Request):
# Refuse if not in the right format
if request.headers.get("Content-Type") != "application/x-www-form-urlencoded":
return PlainTextResponse("Invalid body format", 400)

# Break down body
body = await request.body()
body = body.decode()
fields = parse_qs(body)

# Body must have username and password
if "username" not in fields or "password" not in fields:
return PlainTextResponse("Missing username or password", 400)

# Get username and password
email = fields["username"][0]
password = fields["password"][0]

# Found user with this email
if database.get_user_auth(email)[0]:
return PlainTextResponse("User already exist", 403)

# Create salt and password hash
salt = os.urandom(16)
password = password.encode()
hash = hashlib.scrypt(password, salt=salt, n=2, r=64, p=1)
salt = binascii.b2a_hex(salt)
hash = binascii.b2a_hex(hash)

database.create_user(email, salt, hash)

return PlainTextResponse("User created")


async def recover_account(request: Request):
# Get email in body
email = await request.body()
email = email.decode()

# Didn't find user with this email
if not database.get_user_auth(email)[0]:
return PlainTextResponse("No account with this email", 403)

# Create recovery code
code = secrets.token_urlsafe(32)

# Save to database, so we can check it later
database.create_recovery_code(email, code)

# Create email
message = EmailMessage()
message["Subject"] = "Recover account"
message["From"] = "server@localhost"
message["To"] = email
message["Content"] = code

# Send email
s = smtplib.SMTP("localhost", 8025)
s.send_message(message)
s.quit()

return PlainTextResponse("Recovery code sent to your email")


async def change_account(request: Request):
# Break down body
body = await request.body()
body = body.decode()
fields = parse_qs(body)

# Body must have email, code and password
if "email" not in fields or "code" not in fields or "password" not in fields:
return PlainTextResponse("Missing email or password", 400)

# Get email, recovery code and new password
email = fields["email"][0]
code = fields["code"][0]
password = fields["password"][0]

if not email or not code or not password:
return PlainTextResponse("Missing email/code/password", 400)

if not database.is_recovery_code_valid(email, code):
return PlainTextResponse("Invalid code", 403)

# Create salt and new password hash
salt = os.urandom(16)
password = password.encode()
hash = hashlib.scrypt(password, salt=salt, n=2, r=64, p=1)
salt = binascii.b2a_hex(salt)
hash = binascii.b2a_hex(hash)

# Change password and remove recovery code
database.change_account(email, salt, hash)
database.delete_recovery_code(email)

return PlainTextResponse("Password changed")


# Needs to be authenticated to receive this response
@requires("authenticated")
async def content(request: Request):
return PlainTextResponse("Private content")


database.setup()

app = Starlette(
debug=True,
routes=[
Route("/", content),
Route("/register", register, methods=["post"]),
Route("/recover_account", recover_account, methods=["post"]),
Route("/change_account", change_account, methods=["post"]),
],
middleware=[Middleware(AuthenticationMiddleware, backend=AuthBackend())],
)

Agora podemos iniciar o server com:

uvicorn --reload main:app

Client

Script para preparar o diretório dos exemplos:

mkdir client
cd client
python3 -m venv venv
. venv/bin/activate
pip install httpx
touch content.py register.py recover_account.py change_account.py

Client - Access Content

Mesmo que o post anterior porém trocando usuário por email.

content.py
import sys
import httpx
import base64

# Get username and password from command line
username = sys.argv[1]
password = sys.argv[2]

# Setup credentials string
credentials = f"{username}:{password}"
credentials = credentials.encode()
credentials = base64.b64encode(credentials)
credentials = credentials.decode()

# Get content
response = httpx.get("http://127.0.0.1:8000/", headers={"Authorization": f"Basic {credentials}"})
print(response.content)

Execute o código para testar obter o conteúdo do sistema:

python content.py username@email.com password

Se você ainda não escreveu o código de registar, isto deve estar proibindo você de ver o conteúdo da página.

Client - Register User

Mesmo que o post anterior porém trocando usuário por email.

register.py
import sys
import httpx
from urllib.parse import urlencode

# Get email and password from command line
email = sys.argv[1]
password = sys.argv[2]

# Setup body string
body = urlencode({"email": email, "password": password})

# Register user
response = httpx.post(
"http://127.0.0.1:8000/register",
headers={"Content-Type": "application/x-www-form-urlencoded"},
content=body,
)
print(response.content)

Execute o código para registar seu email e senha:

python register.py username@email.com password

Agora se executar novamente o código de pegar conteúdo, deve conseguir ler o conteúdo da página.

python content.py username@email.com password

Client - Recover Account

Nosso endpoint de requisitar senha só precisa do email, então esse código é o mais curto que veremos.

recover_account.py
import sys
import httpx

# Get email from command line
email = sys.argv[1]
content = f"{email}"

# Request recovery code
response = httpx.post("http://127.0.0.1:8000/recover_account", content=content)
print(response.content)

Execute o código para requisitar um email com o código de recuperação:

python recover_account.py username@email.com

Olhe no terminal que está executando o server de email, nele você deve receber um email com o código.

Client - Change Account

Agora podemos cobrar do usuário o código de recuperação ao mesmo tempo que a nova senha.

change_account.py
import sys
import httpx
from urllib.parse import urlencode

# Get email, code and new password from command line
email = sys.argv[1]
code = sys.argv[2]
password = sys.argv[3]

# Setup body string
body = urlencode({"email": email, "code": code, "password": password})

# Change account password
response = httpx.post("http://127.0.0.1:8000/change_account", content=body)
print(response.content)

Após receber o código, execute este código para trocar a senha e lembre de informar o código de recuperação visto no servidor de email:

python change_account.py username@email.com fasdfasfasdfasdf new_password

Tente acessar o conteúdo da página com a senha velha e veja falhar.

python content.py username@email.com password

Tente acessar o conteúdo da página com a senha nova e veja o conteúdo privido.

python content.py username@email.com new_password

Thiago Lages de Alencar

Username com password é uma das maneiras mais velhas de se criar autenticação no seu sistema.

No lado do server utilizaremos:

No lado do client utilizaremos:

  • httpx para fazer requisições

Essa maneira de autenticação envolve armazenar no banco o hash da senha e o salt utilizado durante o hash.

Hash, formalmente conhecido como digest, é o resultado de obtido ao se passar um array de bytes à uma função hash.
Salt, um array de bytes gerado randomicamente para ser usado durante a função hash.

Questions

Por que não salvar a senha direto no banco de dados?

Pois se alguém olhar o banco, vai conseguir todas as senhas dos seus usuários.

O que é função hash?

Uma função que dada uma entrada de bytes, sempre produz a mesma saída de bytes.

funcao("senha_secreta") => "2c005c01d9b373a068941949669ccfb69ef1b4a0315b10313185761803e05e69"

Se você tiver uma a saída de bytes, não temos algoritmo para descobrir qual foi a entrada de bytes.

funcao(???) <= "f00c15643396616a89a0cb79039f740575defe9dbe307cccdaf8ae210e1c9cc6"

O que acontece se alguém conseguir acesso ao banco de dados neste caso?

A pessoa vai ter acesso a todos os hash, mas não vai ter a senha então não vai conseguir acessar a conta.

Como vamos saber se o usuário acertou a senha se não sabemos a senha?

Iremos comparar o hash da senha que o usuário nos der.

Se no banco nos temos o hash fn54978fn435u9gnweru e o usuário nos der uma senha que gere esse hash, então ele sabe a senha certa.

Por que a função hash usa salt?

Se duas pessoas tiverem a mesma senha e jogarem na função hash, o resultado será o mesmo.

usuário 1 => senha_secreta => "2c005c01d9b373a068941949669ccfb69ef1b4a0315b10313185761803e05e69"
usuário 2 => senha_secreta => "2c005c01d9b373a068941949669ccfb69ef1b4a0315b10313185761803e05e69"

Se gerarmos algo aleatório para adicionar na entrada da função hash, o resultado muda completamente.

usuário 1 => uejfisenha_secreta => "1c131f765a97e3bcf5101cd3e9e269e552716ad30be97e54f852e4f78ed90e44"
usuário 2 => 2ncWQsenha_secreta => "81feed1d27d67d6da5ebfa18ce58255d2822d95d286161fa3e42e323415d8263"

Agora ninguém sabe que os dois usuários tem a mesma senha, porém vamos precisar salvar o salt para reproduzir o resultado.

Server

Script para preparar o diretório dos exemplos:

mkdir server
cd server
python3 -m venv venv
. venv/bin/activate
pip install duckdb starlette uvicorn
touch main.py auth.py database.py

Server - Database Operations

Faremos apenas 3 operações no nosso banco de dados durante este post:

  • Preparar o banco
    • Não é boa prática fazer isto por código, estamos fazendo para ter um projeto com tudo incluso
  • Pegar autenticação do usuário
  • Criar usuário
database.py
import duckdb


def setup():
cursor = duckdb.connect("users.db")

cursor.execute("""
CREATE SEQUENCE IF NOT EXISTS user_id_sequence START 1
""")

cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id integer primary key default nextval('user_id_sequence'),
user text not null unique,
salt text not null,
hash text not null
)
""")


def get_user_auth(user: str) -> tuple[str, str]:
cursor = duckdb.connect("users.db")
result = cursor.execute(
"SELECT salt, hash FROM users WHERE user = $user",
{"user": user},
).fetchone()

if result:
return (result[0], result[1])
return ("", "")


def create_user(user: str, salt: str, hash: str) -> bool:
cursor = duckdb.connect("users.db")
cursor.execute(
"""
INSERT INTO users (user, salt, hash) VALUES
($user, $salt, $hash)
""",
{"user": user, "salt": salt, "hash": hash},
)

Server - Authentication

Código que sempre ira rodar para validar a autenticação do usuário em páginas que precisam de autenticação.

Na framework Starlette isso pode ser feito dentro de um Middleware.

auth.py
import base64
import hashlib
import binascii

from starlette.authentication import (
AuthenticationBackend,
AuthenticationError,
AuthCredentials,
SimpleUser,
)
from starlette.requests import HTTPConnection

from database import get_user_auth


class AuthBackend(AuthenticationBackend):
async def authenticate(self, conn: HTTPConnection):
# No authroization, no access
if "Authorization" not in conn.headers:
return

auth = conn.headers["Authorization"]
scheme, credentials = auth.split()

# Wrong authorization scheme
if scheme.lower() != "basic":
return

# Extract username and password from credentials
credentials = base64.b64decode(credentials)
credentials = credentials.decode()
username, _, password = credentials.partition(":")

# Get real hash from database
salt, hash = get_user_auth(username)
if not salt or not hash:
raise AuthenticationError("Invalid credentails")

# Generate guess of hash
password = password.encode()
salt = binascii.a2b_hex(salt)
guess = hashlib.scrypt(password, salt=salt, n=2, r=64, p=1)
guess = binascii.b2a_hex(guess)
guess = guess.decode()

# Check if the hash guess is the same as real hash
if hash != guess:
raise AuthenticationError("Wrong password")

return AuthCredentials(["authenticated"]), SimpleUser(username)

Note que hashlib.scrypt() retorna bytes e que eles podem não ser conversiveis para utf-8, ascii, etc...

Então para facilitar a comparação, convertemos para uma string hexadecimal e armazenamos desta maneira.

Server - Main

Criamos 2 endpoints:

  • Registrar usuário
  • Acessar um conteúdo exclusivo para usuários
main.py
import os
import hashlib
import binascii
from urllib.parse import parse_qs

from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.requests import Request
from starlette.routing import Route
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.authentication import requires

import database
from auth import AuthBackend


async def register(request: Request):
# Refuse if not in the right format
if request.headers.get("Content-Type") != "application/x-www-form-urlencoded":
return PlainTextResponse("Invalid body format", 400)

# Break down body
body = await request.body()
body = body.decode()
fields = parse_qs(body)

# Body must have username and password
if "username" not in fields or "password" not in fields:
return PlainTextResponse("Missing username or password", 400)

# Get username and password
username = fields["username"][0]
password = fields["password"][0]

# Found user with this username
if database.get_user_auth(username)[0]:
return PlainTextResponse("User already exist", 403)

# Create salt and password hash
salt = os.urandom(16)
password = password.encode()
hash = hashlib.scrypt(password, salt=salt, n=2, r=64, p=1)
salt = binascii.b2a_hex(salt)
hash = binascii.b2a_hex(hash)

database.create_user(username, salt, hash)

return PlainTextResponse("User created")


# Needs to be authenticated to receive this response
@requires("authenticated")
async def content(request: Request):
return PlainTextResponse("Private content")


database.setup()

app = Starlette(
debug=True,
routes=[Route("/", content), Route("/register", register, methods=["post"])],
middleware=[Middleware(AuthenticationMiddleware, backend=AuthBackend())],
)

Agora você pode iniciar o server com:

uvicorn --reload main:app

Client

Script para preparar o diretório dos exemplos:

mkdir client
cd client
python3 -m venv venv
. venv/bin/activate
pip install httpx
touch content.py register.py

Client - Access Content

A convenção para se enviar usuário e senha é:

  • Concatenar usuário e senha com :
    • username:password
  • Transformar em base64
    • dXNlcm5hbWU6cGFzc3dvcmQ=
  • Escrever antes o scheme utilizado
    • Basic dXNlcm5hbWU6cGFzc3dvcmQ=
  • Enviar no campo Authorization do headers
content.py
import sys
import httpx
import base64

# Get username and password from command line
username = sys.argv[1]
password = sys.argv[2]

# Setup credentials string
credentials = f"{username}:{password}"
credentials = credentials.encode()
credentials = base64.b64encode(credentials)
credentials = credentials.decode()

# Get content
response = httpx.get("http://127.0.0.1:8000/", headers={"Authorization": f"Basic {credentials}"})
print(response.content)

Execute o código para testar obter o conteúdo do sistema:

python content.py username password

Se você ainda não escreveu o código de registar, isto deve estar proibindo você de ver o conteúdo da página.

Client - Register User

No caso de registrar, o campo Content-Type no headers deve conter application/x-www-form-urlencoded.

Enquanto a convenção do body é:

  • Concatenar nome do campo e a informação do campo com =
    • username=USERNAME
  • Concatenar todos os grupos de informação com &
    • username=USERNAME&password=PASSWORD
  • Enviar no body do request
register.py
import sys
import httpx
from urllib.parse import urlencode

# Get username and password from command line
username = sys.argv[1]
password = sys.argv[2]

# Setup body string
body = urlencode({"username": username, "password": password})

# Register user
response = httpx.post(
"http://127.0.0.1:8000/register",
headers={"Content-Type": "application/x-www-form-urlencoded"},
content=body,
)
print(response.content)

Execute o código para registar o usuário e senha:

python register.py username password

Agora se executar novamente o código de pegar conteúdo, deve conseguir ler o conteúdo da página.

python content.py username password

References

Thiago Lages de Alencar

Esse post é meio que uma compilação do que eu entendi de cada assunto após de horas lendo na internet e perguntando para meu pai.
Em outras palavras: Pode ter informação incorreta!

TTY

Teletype
(https://en.wikipedia.org/wiki/Teleprinter)

TTY é um comando em linux para saber o nome do terminal o qual a input está diretamente conectada.

$ tty
/dev/pts/1

$ echo "example" | tty
not a tty

No primeiro exemplo, o comando tty veio como input diretamente do terminal.

No segundo exemplo, o comando tty recebeu a input example do comando anterior (não de um terminal).

Fim! Pode ir para a próxima sessão, ao menos que você queira saber o que diabos é uma teletype.

Rosto curioso

Já notou que muitas coisas no computador possuem o nome de objetos que existem fora do computador?

Acontece que o nome é dado baseado nestes objetos para ajudar usuários a entenderem melhor o uso deles no computador! Por exemplo:

  • file
  • folder
  • trash can
  • window

O mesmo vale para TTY, onde o nome veio de teletypes. Infelizmente não é um nome que ajude muito pois computadores já subsittuiram o uso delas então esse nome não ajuda ninguém a saber do que o comando se trata 🤣.

O que são teletypes?
Entenda que elas são uma junção de typewriters e telegraph key.
O primeiro utilizado para escrever em papel e o segundo utilizado para enviar morse code a distância.

Morse code era muito utilizado como uma forma de comunicação binária via cabo (som curto/longo), então teletypes desenvolveram lógicas para converter esses sinais para letras e vice-versa. Por isto a chegada delas subistituiu tradutores de morse code, elas viraram como novo meio de enviar e receber mensagens.

Ao mesmo tempo computadores e terminais estavam nascendo. Alguns terminais vinham com capacidade de receber/escrever informação das/nas teletypes (afinal é tudo binário).

Uma coisa que não ficou claro para mim é se inicialmente teletypes eram usadas para enviar/receber mensagem dos computadores iniciais.
Bem, a essa altura pelo menos ficou claro que a funcionalidade a qual elas foram inspiradas era a capacidade de enviar/receber dados.

Este video mostra uma teletype recebendo e enviando dados de/a um terminal:
https://www.youtube.com/watch?v=S81GyMKH7zw

Por isto que o termo TTY era utilizado para referência aparelhos enviando/recebendo (input/output) mensagem do computador.

Algumas linguagens até incluem código para fazer essa verificação:

  • C
    • #include <unistd.h>

      isatty(fildes);
  • Python
    • import os

      os.isatty(fd)
  • NodeJS
    • tty.isatty(fd)

Onde a funcionalidade das funções é identificar se a input/ouput está vinculada a um aparelho (device).

warning

Preste bem atenção que sua input pode estar ligada ou não a um aparelho E sua output pode estar ligada ou não a um aparelho.
Um deles estar ligado não quer dizer que ambos estão.

As funções recebem um file descriptor e dizem se ele está ou não linkado a um aparelho.
Você poderia passar STDIN, STDOUT ou até STDERR para a função analisar.

Esse video cobre bem o assunto: https://www.youtube.com/watch?v=SYwbEcNrcjI

Se realmente quiser saber detalhes sobre TTY, existe este blog cheio de informações (que eu não li):
https://www.linusakesson.net/programming/tty/

TTY

Terminal

(https://en.wikipedia.org/wiki/Computer_terminal)

Teletypes originalmente eram conhecidas como "hard-copy terminals" por usarem papel, mas com a vinda de telas nós formamos uma nova ideia de terminal nas nossas cabeças (a tela preta).

Terminal não possui armazenamento de dados, da mesma maneira que teletypes apenas eram responsáveis por ler e escrever do computador, ou seja, a lógica ainda estava no computador. Alguns terminais possuiam um pouco de lógica neles porém nada comparado ao computador.

Se você viu o video da sessão anterior então já deve ter ganhado uma ideia do que é um terminal, pois nele é mostrado uma teletype lendo e escrevendo para um terminal.
Mas caso queira outro video mostrando melhor um terminal:
https://www.youtube.com/watch?v=UNdu0YQfvF0

Terminal

Terminal Emulator

(https://en.wikipedia.org/wiki/Terminal_emulator)

Hoje em dia usamos o termo terminal para representarmos emuladores de terminais.

Diferentemente de terminais, estes estão fortemente ligados a computador e não são máquinas separadas da lógica. Basicamente estamos falando da janela que finge ser um terminal (GNOME terminal).

Terminal Emulator

Shell

(https://en.wikipedia.org/wiki/Shell_script)

Um programa responsável por ficar em loop esperando comandos do usuário para serem executados.

Comandos podem ser:

  • Programas
    • echo
    • ls
    • touch
    • mkdir
    • Buscados em lugares pré definidos (/bin, /usr/bin, ...)
      • Use echo $PATH para ver a lista de lugares a se olhar
  • Comandos do próprio shell
    • type
    • which
    • help
    • man
    • Estes existem dentro do shell e não precisam ser buscados.
  • Shell functions
  • Aliases
    • Comandos definidos por nós, construido de outros comandos

Existem variações e alternativas de shell:

Shell

CLI

Command-line interface
(https://en.wikipedia.org/wiki/Command-line_interface)

É uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa.

Está interface se basea no usuário passar flags e mais informações em conjunto ao comando, dessa maneira mudando o comportamento do commando. Por exemplo, o programa ls disponibiliza diversas flags para alterar o comportamento:

  • ls
    • Lista tudo no diretório atual mas ignora os começando com .
  • ls -a
    • Lista tudo no diretório atual e não ignora os começando com .
  • ls -l
    • Lista tudo no diretório atual mas com mais detalhes

Fique bem claro que é o programa te dando opções de como interagir com ele, não o shell ou terminal, então resta ao programa implementar comportamentos para certas flags.

note

É muito comum programas oferecerem detalhes sobre as flags quando utilizando a flag --help (ls --help).

CLI

TUI

Terminal user interface
(https://en.wikipedia.org/wiki/Text-based_user_interface)

Novamente é uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa. Porém está foca em dar uma interação mais visual e continua.

Diferente de CLI's onde toda a interação começa e termina em um comando só, TUI's continuam esperando mais interações do usuário até um dos dois decidirem terminar.

Um exemplo bem comum é top que providência uma visão dos programas/processos/threads em execução do sistema, uma vez inicializado ele esperar por mais interações do usuário. Se você apertar q ele termina, se você apertar h ele fornece a lista de comandos, etc.

Note que a TUI's ainda podem providênciar flags para alterar o comportamento (top --help).

TUI

GUI

Graphical user interface
(https://en.wikipedia.org/wiki/Graphical_user_interface)

Novamente é uma interface, ou seja, maneira do programa dar mais controle ao usuário sobre o programa. Porém não está limitada a usar texto para a visualização, pois tem a capacidade de desenhar na tela.

Hoje em dia é o meio mais popular de se usar uma aplicação, quando se abre VSCode, Google Chrome, Discord... Todos são GUI's pois utilizaram a capacidade de desenhar para dar uma interface ao usuário.

Mesmo programas focados em GUI's podem aceitar flags (VSCode: code --help).

GUI

References

Thiago Lages de Alencar

Durante o post C/C++ Libs escrevi sobre usar bibliotecas no Linux e nunca imaginaria que teria novamente a dor de cabeça de ver o assunto quando fosse fazer o mesmo no Windows.

Personagem do desenho The Owl House chorando

warning

Importante avisar que eu irei escrever este post contando que você leu o post passado sobre Linux.

Irei ser breve e não irei estudar detalhes que nem fiz no do linux (pois estou cansado do assunto).

.lib (library)

Basicamente idêntico ao .a do Linux, uma biblioteca estática (static library).

note

Descobri que estes arquivos podem ser abertos tranquilamente com programas de zip tipo: Winrar, 7zip, PeaZip.

O que faz total sentido dado que eles programas para lidar com "File archive and extractor" e no Linux usando o formato .a de "archive".

Seria o próximo do .so do Linux, servindo o mesmo propósito de uma biblioteca compartilhada (shared library) porém o Windows possui uma implementação própria deles (dynamic-link library).

A grande diferença que precisamos saber é que durante a criação de bibliotecas compartilhadas, três formatos de arquivos são criados: .dll, .lib e .exp (vamos ignorar este último).

Onde o arquivo .lib NÃO é o mesmo que o gerado durante a biblioteca estática! Porém ainda é necessário para a utilização da biblioteca compartilhada.

Project from Zero

Seguiremos a mesma ideia do post no Linux. Apenas tendo o nosso código:

project/
└── src/
├── main.c
└── ...
cl src\main.c

Agora com um header:

project/
├── include/
│ └── header.h
└── src/
├── main.c
└── ...
cl src/main.c /Iinclude

.lib

project/
├── include/
│ └── header.h
├── lib/
│ └── name.lib
└── src/
├── main.c
└── ...
cl src/main.c /Iinclude lib/name.lib
note

Windows não tem o padrão de botar lib na frente das bibliotecas então não precisa nem pensar nisso.

.dll

Dessa vez vamos separar em duas etapas: compilar o código para .obj e depois linkar (gerar o .exe), isso torna mais fácil adicionar explicações no meio.

project/
├── include/
│ └── header.h
├── lib/
│ └── name.lib
└── src/
│ ├── main.c
│ └── ...
└── name.dll

Lembre que bibliotecas dinâmicas no Windows utilizam um arquivo .dll e um .lib (por isso temos os dois no projeto).
Windows busca os arquivos .dll na mesma pasta do executável, por isso ele não está na pasta lib.

cl src/main.c /c /Iinclude /Dexample

/c é justamente para pausar antes de linkar.
/Dexample é uma maneira de adicionar uma definição no início do código, equivalente a

#define example

Por que precisamos definir? Em bibliotecas do Windows, muitas vezes pode se encontrar código como o seguinte:

#if defined(EXPORT_DLL)
#define LIB_API __declspec(dllexport)
#elif defined(IMPORT_DLL)
#define LIB_API __declspec(dllimport)
#else
#define LIB_API
#endif

Onde LIB_API é substituido por:

  • __declspec(dllexport) quando criando uma biblioteca dinânmica
  • __declspec(dllimport) quando importando uma biblioteca dinâmica
  • nada quando é uma biblioteca estática

Não entendo bem do assunto então não pretendo entrar no assunto, mas é a maneira do windows lidar com bibliotecas dinâmicas.

Biblitoecas geralmente requerem que você passe essa definição para que ela adicione o contexto certo ao código durante a criação do objeto (para fazer name mangling corretamente?).


link /LIBPATH:lib name.lib main.obj

É durante a etapa de linkar que o arquivo .lib é finalmente utilizado!

Lembrando novamente que o executável vai buscar o .dll na pasta do executável, então bote ambos juntos.

Thiago Lages de Alencar

Forward And Backward Reaching Inverse Kinematics (FABRIK) é a última kinematic que veremos. É utilizada para cadeias de ossos também mas apenas precisamos de uma iteração para definir o estado final (diferentemente da CCD).

Corrente de 4 ossos

Forward And Backward Reaching

A idéia é duas caminhadas na cadeia de ossos, a primeira irá mover os ossos em direção ao alvo (forward) e a segunda vai voltar o osso a base (backward).

A operação importante a se entender durante as duas etapas é ação de alcançar um outro ponto (reaching). Vamos utilizar ela durante as duas etapas então é bom entender isto primeiro.

Reaching

Alcançar um alvo é dividido em 2 etapas, olhar para o alvo e mover até o alvo.

Um osso e um alvo fora do alcance

Na primeira etapa podemos utilizar a mesma lógica do look at ou usar a função que sua game engine disponibilizar para rotacionar até um ponto.

Osso rotacionando até o alvo

Para mover até o ponto é preciso usar o tamanho do osso e calcular onde seria o novo ponto da base do osso. Visualizar onde seria é algo bem simples:

Osso transparente onde o osso precisa estar no final

Calcular isso envolve conseguir criar um vetor que represente o osso. Primeiro precisamos saber o vetor em que o osso se encontra, o vetor do ponto inicial dele até o alvo.

Vetor = Posição do alvo - Posição do osso

Vetor do osso até o alvo

Com isto podemos calcular a proporção desse vetor com o vetor do osso. Em outras palavras, qual o tamanho do vetor do osso em relação a esse vetor? É duas vezes o tamanho deste? É três vezes o tamanho? É metade desse vetor?

Tamanho do vetor = √(Vetor.x² + Vetor.y²)
Proporção = Tamanho do osso / Tamanho do vetor

Utilizando essa proporção podemos criar um vetor do tamanho do osso.

Vetor osso = Vetor * Proporção

Vetor osso

A última coisa é calcular o novo ponto onde o osso deve inciar. Basta pegar o ponto do alvo e reduzir pelo vetor do osso.

Posição do osso = Posição do alvo - Vetor osso

Vetor osso começando no ponto onde o osso tem que terminar

Pronto, sabemos onde botar o osso e podemos mover ele para lá (caso você já não tenha movido ele na última operação)

Osso na posição correta

Forward

A primeira caminhada pela cadeia de ossos envolve fazer cada osso andar em direção (forward) ao osso seguinte. No caso da ponta da cadeia, ela irá mover em direção ao alvo.

TODO

TODO

O ponto vermelho irá representar onde globalmente o início do osso atual está, nós usamos ele para decidir onde o osso seguinte vai alcançar.

TODO

TODO

TODO

Estou pausando aqui para lembrar que movimentar e rotacionar um osso afeta todos os filhos, por isto os ossos filhos são movimentados e rotacionados de forma a ficarem "piores" (mais longe do alvo).

TODO

O osso seguinte irá utilizar o osso anterior como referência, seguimos essa tática para cada um dos ossos.

TODO

TODO

TODO

TODO

TODO

TODO

O ponto azul representa a base da cadeia e é um ponto de referência que usaremos na próxima caminhada.

Backward

note

A última caminhada deixou tudo uma bagunça mas isso apenas porque eu escolhi trata-los da mesma forma que minha game engine (Godot) trata nodes nela.

Se tivessemos usado um array de ossos em vez de relações pai e filho, um não afetaria o outro!

Nós focaremos agora a mover os ossos em direção a base, ou seja, eles caminharam para tráz (backward). Dessa vez não precisamos nos preocupar em rotacionar, apenas mover para o final do osso anterior.

TODO

TODO

TODO

TODO

TODO

TODO

TODO

TODO

Iteration

Ao final de uma iteração podemos ter algo errado como visto acima, mas se repetirmos mais vezes vamos começar a caminhar para algo melhor.

O que acontece se começarmos outra iteração? Vamos começar novamente rotacionando o osso da ponta.

TODO

Se você der zoom na imagem, vai notar que o último osso passou do ponto alvo. Mas a etapa seguinte é mover de forma que o ponto final do osso bata com a posição do alvo.

TODO

Bem, agora o osso seguinte está incorreto... Mas se continuarmos repetindo o processo...

TODO

Aos poucos os ossos vão indo para uma posição melhor, mas eu não pretendo mostra-lo uma segunda iteração pois eu fiz estes desenhos a mão. 🤣

Conclusion

O código simplificado em GDScript (linguagem do Godot):

for i in iterations:
_apply_forwards()
_apply_backwards(base_global_position)


func _apply_forwards() -> void:
# O osso da ponta vai morar no alvo, os seguintes vão tratar o anterior como alvo.
var target_global_position: Vector2 = target.global_position

# Esse array leva da ponta até a base.
for bone in chain:
# Rotaciona em direção ao alvo.
bone.look_at(target_global_position)

# Evita calcular ratio como infinito.
if target_global_position == bone.global_position:
continue

# Calcula a nova posição do osso.
var stretch: Vector2 = target_global_position - bone.global_position
var ratio: float = bone.get_bone_length() / stretch.length()
bone.global_position = target_global_position - stretch * ratio

# Define o alvo do osso seguinte.
target_global_position = bone.global_position


func _apply_backwards(base_global_position: Vector2) -> void:
# Esse array leva da ponta até a base, então agora precisamos caminhar ao contrário.
for i in range(chain.size() - 1, -1, -1):
var bone: Bone = chain[i]

bone.global_position = base_global_position

# Calcula a posição do osso seguinte.
var direction := Vector2(cos(bone.global_rotation), sin(bone.global_rotation))
base_global_position = bone.global_position + direction * bone.get_bone_length()

References

Thiago Lages de Alencar

Cyclic Coordinate Descent Inverse Kinematic (CCDIK) é diferente das lógicas anteriores, pois nós não sabemos qual o estado final que desejamos para os ossos. A ideia é fazer diversas iterações até que chegue em um resultado aceitável.

Em outras palavras, CCDIK se trata da jornada e não do resultado final.

Corrente de 4 ossos

Cyclic Coordinate Descent

Para cada osso temos que calcular a rotação para a ponta chegue mais perto do ponto desejado.

Dependendo da direção que você caminhar pela cadeia de ossos o movimento pode ser diferente. Nesse exemplo iremos fazer de tráz para frente (osso mais perto da ponta até osso mais longe da ponta).

Rotacionando osso 4

Rotacionnado osso 3

Rotacionnado osso 2

Rotacionnado osso 1

Após primeira iteração

E com isto fizemos a primeira iteração. Vamos começar a segunda iteração.

Rotacionando osso 4

Rotacionnado osso 3

Rotacionnado osso 2

Rotacionnado osso 1

Após segunda iteração

Cada iteração se aproximando mais do ponto desejado.

Após N iterações

O único cálculo que precisamos fazer toda iteração é o ângulo da ponta até o alvo.

Negative Scale

Recomendo ler no Two Bone sobre escala negativa. O que importa é que o mesmo se aplica neste caso, se uma das escalas for negativa, precisamos rotacionar na direção oposta.

Conclusion

Em GDScript o código seria algo como:

for bone in chain:
var angle_to_target: float = bone.global_position.angle_to_point(target.global_position)
var angle_to_tip: float = bone.global_position.angle_to_point(tip.global_position)
var angle_diff: float = angle_to_target - angle_to_tip

# Escala negativa ou não.
if bone.global_scale.sign().x == bone.global_scale.sign().y:
bone.rotate(angle_diff)
else:
bone.rotate(-angle_diff)
note

Normalmente toda iteração você verificaria se chegou em um resultado aceitável.

No meu caso (game engine), estou fazendo uma iteração por frame e sem pensar se chegou ou não em um resultado aceitável.

References

Thiago Lages de Alencar

Faz um mês desde que escrevi sobre inverse kinematic look at. Talvez eu esteja enrolando para falar desta pois foi por ela que eu comecei a ver inverse kinematics... e sofri muito.

Two bone inverse kinematic! Dado que queremos a mão em uma devida posição, como os dois ossos responsáveis pelo braço devem se encontrar?

Note que não vamos ditar onde a mão vai estar, porém onde desejamos que ela estivesse. Isso é importante pois o calculo muda dependendo se a mão alcança ou não a posição desejada.

Um braço dobrado e com a mão aberta

Two Bone

Braço estendido

O que você faz quando tenta alcançar algo longe de você?
Estica o máximo possível.

O que você faz quando tenta alcançar algo perto de você?
Curva o braço de forma que sua mão acabe na posição desejada.

Primeira coisa a se fazer é descobrir se está fora ou dentro do alcance 🤣.
Em outras palavras, a base do braço até o ponto desejado é maior ou menor que o braço todo?

Braço estendido com vetor para um ponto fora do alcance

Podemos descobrir a distância entre dois pontos se calcularmos o vetor entre eles e depois usarmos a clássica formúla para distância. Resumidamente:

  • P2-P1
  • √(x²+y²)

Sabendo disso podemos calcular as seguintes distâncias:

  • A -> T
    • Distância até posição desejada
  • A -> B
    • Tamanho do osso 1
  • B -> C
    • Tamanho do osso 2

Agora podemos verificar justamente se está dentro ou fora do alcance!

Distância até posição desejada > (Tamanho do osso 1 + Tamanho do osso 2)

Out of Range

Acontece que estender o braço em uma direção é apenas tornar o ângulo global dos ossos equivalentes ao da direção.

Mostrando o ângulo global do vetor

Mostrando o ângulo global do braço quando está na mesma direção do vetor

Já vimos em IK Look at como fazer um osso/vetor apontar para uma direção e isso é tudo que precisamos fazer aqui também.

  • Apontar osso 1 para posição desejada
  • Apontar osso 2 para posição desejada

Fim.

In range - Triangle

Espero que este desenho já deixe claro como utilizaremos trigonometria com braços curvados.

Mostrando que braços curvados podem ser vistos como triângulos

Neste caso o ponto onde desejamos posicionar a mão está dentro do alcance dela, então irá acabar sendo exatamente a posição da mão (utilizaremos C mas poderia ser T).

Mostrando um braço curvado e que utilizaremos as letras A,B,C para representar pontos e a,b,c para representar tamanho do lado do triângulo

Já calculamos os lados do triângulo, então agora vamos focar no seus ângulos internos (utilizaremos α β γ).

Mostrando um braço curvado e que utilizaremos as letras A,B,C para representar pontos e a,b,c para representar tamanho do lado do triângulo

Sabendo todos os lados do triângulo podemos utilizar leis do cossenos para descobrir cada ângulo interno:

a² = b² + c² - 2bc*cos(α)
b² = a² + c² - 2ac*cos(β)
c² = a² + b² - 2ab*cos(γ)

Sabendo os lados e sabendo os ângulos internos nós conseguimos dizer como o braço precisa estar dobrado. O problema é que ele ainda pode estar dessa forma de diversas maneiras 🤣:

Mostrando diferentes maneiras que o braço pode estar rotacionado

In range - Two Angles

Existem dois ângulos que estamos buscando descobrir, rotacionando eles conseguiremos os ossos exatamente onde queremos:

Mostrando rotação por rotação a se fazer em um braço que está inicialmente apontando para o eixo X

Nessa imagem o braço estava esticado em direção ao eixo X, rotacionamos osso 1 por θ1 e osso 2 por θ2 para obter o braço no formato que queriamos.

note

Eu sei que os desenhos tem ficado cada vez piores, eu deveria estar usando uma ferramenta apropriada ou organizando melhor os desenhos...

Mas a preguiça ganhou 🙂

Como podemos obter θ1?

Se você estava pensando "é só calcular o ângulo do eixo X até o osso 2 que você consegue o θ1", deixe-me lembra-lo que o braço vai começar de forma desconhecida.

Mesmo se estivesse esticado no eixo X, o osso 2 não vai estar na posição desejada ainda!

Mostrando o braço no eixo X e o ponto desejado acima dele

Mas sabe o que podemos fazer? Calcular o ângulo do eixo X até o ponto desejado (T).

Mostrando o ângulo do eixo X até o vetor feito do osso 1 até o ponto desejado

Sabe o porque eu chamei ele de α'? Porque ele está relacionado com α!

Acontece que para obter o ângulo desejado, podemos rotacionar até a direção de T e depois remover a rotação interna do triângulo (α).

Mostrando os ângulos α&#39; e α

Não precisamos literalmente rotacionar, podemos calcular o ângulo e depois rotacionar: α' - α

Mostrando que se reduzirmos α&#39; pelo ângulo interno α conseguimos o osso 1 apontando na direção certa

Como podemos obter θ2?

Felizmente o osso 2 não rotacionado faz um ângulo de 180º com o osso 1.

Mostrando que o osso 2 quando tem rotação 0º, faz um ângulo de 180º com osso 1

Se rotacionarmos por 180º e diminuirmos pelo ângulo interno (β), obtemos justamente o ângulo que queriamos.

Mostrando o ângulo de 180º e β para melhor ver que é possível conseguir o ângulo do osso 2

Novamente não precisamos literalmente rotacionar, podemos calcular o ângulo e depois rotacionar: 180º - β

Mostrando que se reduzirmos β do 180º conseguimos o osso 2 apontando corretamente

No final chegamos aos ângulos graças aos ângulos internos do triângulo:

θ1 = α' - α
θ2 = 180º - β

In range - Bend Direction

Mas se nós quisermos que o braço fique curvado para o outro lado?

Acontece que mesmo curvando para o outro lado, os valores internos do triângulo não se alteram.

Mostrando que mudar a direção que o braço curva não afeta o triângulo interno

Então todo o calculo se mantém até a última etapa, onde precisamos mudar o sinal da rotação interna.

θ1 = α' + α
θ2 = 180º + β

In range - Negative Scale

Quando você escala qualquer um dos eixos por negativo, você também está dizendo que a direção para qual ele está rotacionando trocou:

Vetor (1,1) antes e após escalar X por -1

Se agora escalarmos o eixo Y negativamente, a rotação irá voltar a ser igual o início.
Cada vez que você escala um eixo negativamente, você troca a direção das rotações.

Como isso afeta nossos calculos?

Apenas o ângulo que utiliza o eixo X como referência é afetado (pois o eixo X nunca é escalado negativamente)

Mesma imagem anterior porém mostrando o segundo ângulo do ponto de vista do eixo X

Agora não queremos reduzir do ângulo α', mas sim acrescentar:

θ1 = α' + α

Mas se quisermos o osso curvado para a outra direção? É, então queremos novamente reduzir...

θ1 = α' - α

Err... basicamente estamos bricando de jogo do troca, dependendo da situação queremos rotacionar para diferentes direções.

Conclusion

Este é o meu código escrito em GDScript (linguagem do Godot):

var flip_bend: bool = false
var target_distance: float = bone_one.global_position.distance_to(target.global_position)
var bone_one_length: float = bone_one.get_bone_length()
var bone_two_length: float = bone_two.get_bone_length()
var angle_to_x_axis: float = (target.global_position - bone_one.global_position).angle()

# Fora do alcance.
if target_distance > bone_one_length + bone_two_length:
bone_one.global_rotation = angle_to_x_axis
return

# Lei dos cossenos.
var angle_0: float = acos(
(target_distance ** 2 + bone_one_length ** 2 - bone_two_length ** 2) / (2 * target_distance * bone_one_length)
)

var angle_1: float = acos(
(bone_two_length ** 2 + bone_one_length ** 2 - target_distance ** 2) / (2 * bone_two_length * bone_one_length)
)

# Direção da curva do braço.
if flip_bend:
angle_0 = -angle_0
angle_1 = -angle_1

# Escala negativa ou não.
if bone_one.global_scale.sign().x == bone_one.global_scale.sign().y:
bone_one.global_rotation = angle_to_x_axis - angle_0
else:
bone_one.global_rotation = angle_to_x_axis + angle_0

bone_two.rotation = PI + angle_1

Extra - Negative Scale in Godot

Este é extra pois depende muito da ferramenta que está utilizando, no meu caso Godot em 2D.

Godot representa translação, rotação e escala utilizando matriz. Entenda mais sobre transforms na documentação do Godot, aqui iremos direto ao assunto.

Matriz identidade representa um transform sem alteração nenhuma (translação, rotação e escala)

Matriz identidade

A desvantagem de utilizar uma matriz para armazenar todas essas informações é que algumas são impossíveis de extrarir corretamente. Olhe a matriz após escalar X por -1:

Matriz com X escalado por -1

Agora olhe a matriz após rotacionar por 180º e escalar Y por -1:

Mesma matriz apresentada anteriormente

Exatamente a mesma matriz... Se você der essa matriz para Godot, ele vai assumir que você fez a segunda opção (rotacionou e escalou Y por -1).

Como isso afeta nossa Inverse Kinematic?

Não afeta se você utilizou funções que já levam esse problema em conta, porém se vc operou diretamente sobre os transforms... Você talvez note alguns problemas.

References

Thiago Lages de Alencar

Não bastava meu sofrimento com utilização de bibliotecas, agora tive mais dor de cabeça por elas virem do package manager (apt).

Meu sofrimento foi enquanto tentava usar a biblioteca GTK em C.

Rosto sorrindo com os olhos de forma idiota (e com um chápeu de mágico)

Installing

Duas opções:

  • Instalar o pacote pronto da minha distribuição
  • Baixar e compilar o código fonte

Como eu ainda tenho algum amor por mim mesmo, fui pela primeira opção:

$ sudo apt install libgtk-4-1 libgtk-4-dev

E podemos descobrir os arquivos que estes pacotes trouxeram com:

$ dpkg -L libgtk-4-1 libgtk-4-dev

O que nos mostra que o pacote responsável por desenvolvimento (libgtk-4-dev) trouxe muitos headers files e uma biblioteca compartilhada (/usr/lib/x86_64-linux-gnu/libgtk-4.so).

Alguns nomes de headers

Code

Qual será o grande código utilizado durante este post???

#include <gtk/gtk.h>

int main() { return 0; }

Exatamente! Estou cagando para o código, apenas quero acessar a biblioteca!

¿Donde esta la biblioteca?

Rosto do Deadpool

Includes

Talvez seja meio óbvio mas o código não vai ser executado com um simples

$ clang -o my_project main.c

Pois o compilador não irá encontrar a biblioteca.

$ clang -o my_project main.c
main.c:1:10: fatal error: 'gtk/gtk.h' file not found
#include <gtk/gtk.h>
^~~~~~~~~~~
1 error generated.

Existe duas maneiras de adicionar headers ao seu código:

  • #include "biblioteca.h"
    • Dentro do seu diretório atual, busque o arquivo biblioteca.h.
  • #include <biblioteca.h>
    • Dentro dos diretório padrões, busque o arquivo biblioteca.h.
info

Ambos os tipos aceitam caminhos para o arquivo. Exemplo:
#include "dir1/dir2/biblioteca.h"
#include <dir1/dir2/biblioteca.h>

E aspas também podem ser usadas para buscar em diretórios padrões...
Mas se você faz isso, você é um criminoso.

Qual o diretório padrão para bibliotecas? /usr/include

Lembra quando vimos a lista dos arquivos que o pacote trouxe? Muitos headers foram justamente para o diretório padrão.

...
/usr/include/gtk-4.0/gtk/gtkshortcutmanager.h
/usr/include/gtk-4.0/gtk/gtkshortcutsgroup.h
/usr/include/gtk-4.0/gtk/gtkshortcutssection.h
/usr/include/gtk-4.0/gtk/gtkshortcutsshortcut.h
/usr/include/gtk-4.0/gtk/gtkshortcutswindow.h
...

Perfeito então, podemos alterar nosso código para usar #include <gtk-4.0/gtk/gtk.h>

#include <gtk-4.0/gtk/gtk.h>

int main() { return 0; }

E vai dar tudo cert.... ei...

$ clang -o my_project main.c
In file included from main.c:1:
/usr/include/gtk-4.0/gtk/gtk.h:29:10: fatal error: 'gtk/css/gtkcss.h' file not found
#include <gtk/css/gtkcss.h>
^~~~~~~~~~~~~~~~~~
1 error generated.

Nope!

emoticon :^)

Packages

Está é a organização do desenvolvedor da biblioteca:

project/
├── gdk
├── gsk
├── gtk
└── unix-print

Para eles realmente é #include <gtk/gtk.h> por isso outras partes do código deles utiliza assim!
E não é como se eles fossem ficar alterando em todos os arquivos do projeto para referênciar o caminho com versão mais recente do projeto.

Queremos é dizer ao compilador os diretórios a procurar os arquivos...
Pera, nós já fizemos isso no post anterior, usando a flag -I.

#include <gtk/gtk.h>

int main() { return 0; }

Agora basta executar utilizando a flag e sucess... ei...

$ clang -o my_project main.c -I/usr/include/gtk-4.0
In file included from main.c:1:
In file included from /usr/include/gtk-4.0/gtk/gtk.h:29:
/usr/include/gtk-4.0/gtk/css/gtkcss.h:29:10: fatal error: 'glib.h' file not found
#include <glib.h>
^~~~~~~~
1 error generated.

Você esqueceu que está biblioteca pode usar outras bibliotecas e precisamos adicionar o diretório delas também!

emoticon :^) feito de emoticons :^) menores

Dependecies

Existe um programa justamente para ajudar a descobrir as depêndencias de um módulo.

$ pkg-config --cflags gtk4
-I/usr/include/gtk-4.0 -I/usr/include/pango-1.0 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -I/usr/include/harfbuzz -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/fribidi -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/x86_64-linux-gnu -I/usr/include/graphene-1.0 -I/usr/lib/x86_64-linux-gnu/graphene-1.0/include -mfpmath=sse -msse -msse2 -pthread

$ pkg-config --libs gtk4
-lgtk-4 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -lgdk_pixbuf-2.0 -lcairo-gobject -lcairo -lgraphene-1.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0

Mas como se descobre qual o nome do módulo da minha biblioteca? Não sei, se você souber, me conte!

tip

Utilizando o pkg-config --list-all você consegue uma lista de todos os módulos mas nada me disse explicitamente que o módulo de libgtk-4-dev é gtk4.

Talvez seja o Source: gtk4 quando utilizando apt show libgtk-4-1...
Mas não sei ¯\_(ツ)_/¯

Graças a este programa podemos gerar facilmente as flags e finalmente executar o código!

$ clang `pkg-config --cflags gtk4` -o my_project main.c `pkg-config --libs gtk4`

headers entrando numa caixa que representa o binário

Language Server

Sabe aquela ferramenta responsável por completar o código, avisar de errors, te levar à definições...
Bem, ela está reclamando e não queremos deixar ela assim né?.

#include <gtk/gtk.h> // 'gtk/gtk.h' file not found

int main() { return 0; }

Estamos passando diversas informações para nosso compilador (clang) sobre diretórios para utilizar mas não estamos passando nada para o language server (clangd). Podemos fazer um teste rápido para ver o que clangd acha do nosso arquivo com:

$ clangd --check=main.c
...
E[01:35:13.895] [pp_file_not_found] Line 1: 'gtk/gtk.h' file not found
...

Pessoa representando Clang com informações e outra pessoa representando Clangd pedindo também

Project Dependencies

Poderiamos pesquisar e descobrir os argumentos a se passar ao clangd mas isso é algo que varia de projeto a projeto e última coisa que queremos é ficar configurando no Visual Studio os argumentos para cada projeto.

Por isso que clangd por padrão sempre procura configurações do projeto em certos arquivos do projeto como compile_commands.json! Poderiamos escrever este arquivo na mão... mas não queremos então vamos utilizar um programinha amigo chamado bear!

Ele recebe o comando que você está utilizando para criar o executável e cria o compile_commands.json:

$ sudo apt install bear
$ bear -- clang `pkg-config --cflags gtk4` -o my_project main.c `pkg-config --libs gtk4`

Pronto, conseguimos o arquivo de configuração para o clangd.

my_project/
├── compile_commands.json
├── main.c
└── my_project

Basta dar um tempo (ou reniciar o editor de texto) e seu language server deve perceber que está tudo certo!

Fim!
Código funciona!
Language server funciona!
Você está pronto para desenvolver com GTK!
Não se sente uma nova pessoa com todo esse conhecimento?

Rosto cansado

Bem, você não é a primeira pessoa a achar tudo isso desnecessariamente complicado para começar a programar... Outras pessoas criaram ferramentas para ajudar nisso!

Então continue lendo se quiser jogar tudo que viu até aqui no lixo e descobrir uma maneira mais fácil!

Rosto cansado e puto

Meson!

GTK adora falar do Meson e como já estou sofrendo mesmo... Por que não parar pra ver?
Instalando!

$ sudo apt install meson ninja-build

Claro que se vamos testar outra ferramenta, precisamos testar do zero! Apenas com o nosso querido arquivo main.c:

my_project/
└── main.c

Setuping

$ meson init

Isso cria o arquivo de configuração chamado meson.build, nele tem diversas informações mais sofisticadas sobre o projeto:

project('my_project', 'c',
version : '0.1',
default_options : ['warning_level=3'])

executable('my_project',
'main.c',
install : true)

Viu?
Ele não é apenas "estou rodando um código".
Ele é "estou criando um ⭐projeto⭐".

Como ele é um projeto sério, ele vai guardar todas as informações dele em um pasta separada para não sujar o seu projeto ❤️!

Então diga para ele onde botar os arquivos dele (eu escolhi builddir):

$ meson setup builddir

Seu projeto deve acabar com a estrutura:

my_project/
├── builddir/
├── main.c
└── meson.build

Project Dependencies

Sim! Em toda ferramenta é necessário que você a biblioteca que quer usar!
Você não quer que ela ande por todos os diretórios do seu computador procurando a sua biblioteca, né?
Imagina se pega a errada por acidente!

Fazemos isso pelo meson.build!

project('my_project', 'c',
version : '0.1',
default_options : ['warning_level=3'])

executable('my_project',
'main.c',
install : true,
dependencies: dependency('gtk4'))
note

Eu ainda não sei o como se descobre que o nome do módulo é gtk4!

info

Caso você tenha mais que uma dependência, o parâmetro dependencies aceita lista:

executable('my_project',
'main.c',
install : true,
dependencies: [dependency('gtk4')])

Compiling

Pronto! Você está pronto para ter seu projeto compilado sem erro! Talvez warnings coff coff...

Entre no diretório de configuração do Meson (no meu caso builddir) e execute o comando de compilação:

$ cd builddir
$ meson compile
note

Por que entrar no diretório antes? Meson permite que você tenha diversos setups.
Se você estiver fora do diretório é possível rodar o comando se adicionar o argumento para -C:

$ meson compile -C builddir

"clangd está reclamando novamente de 'gtk/gtk.h' file not found!"
Não se desespere pois se você olhar os arquivos criados após compilação, um deles é bem útil.

my_project/
├── builddir/
│ ├── ...
│ ├── compile_commands.json
│ └── my_project
├── main.c
└── meson.build

Olha lá arquivo que o clangd quer!
E o seu executável mas isso era de se esperar...

Copie ele para a raiz do seu projeto e pronto, seu clangd deve parar de chorar erro!

my_project/
├── builddir/
│ ├── ...
│ ├── compile_commands.json
│ └── my_project
├── compile_commands.json
├── main.c
└── meson.build

Conclusion

Meson possui uma etapa de configuração, mas tirando isso os comandos depois ficam bem mais sensatos!

Compilando com clang e atualizando compile_commands.json:

$ clang `pkg-config --cflags gtk4` -o my_project main.c `pkg-config --libs gtk4`
$ bear -- clang `pkg-config --cflags gtk4` -o my_project main.c `pkg-config --libs gtk4`

Compilando com meson e atualizando compile_commands.json:

$ meson compile
$ cp compile_commands.json ../compile_commands.json

Se eu descobrir uma maneira de após compilação já copiar o arquivo compile_commands.json, tudo vai ficar perfeito!

References

Thiago Lages de Alencar
  • Durante a faculdade eu aprendi C

    • Abri Visual Studio
    • Escrevi código em C
    • Executei
  • Recentemente eu usei C++ no projeto Godot

    • Segui instrução do Godot para configurar o VSCode
    • Escrevi código em C++
    • Executei

Em ambos os casos eu nunca aprendi direito sobre bibliotecas... Notei claramente quando segui as instruções de projeto para Build from Source e não sabia mais o que fazer com os arquivos .a e .so gerados.

Desenho de um rosto rindo que nem idiota

Eu sabia que poderia compilar C/C++ com GCC ou Clang, mas meu conhecimento se resumia a compilar projetos 100% meus.

gcc main.c -o main

Bem, vamos entender cada um dois formatos primeiro.

.a (archive)

Se trata de uma biblioteca estática (static library), ou seja, podemos ver como um conjunto de funções/variáveis que seram anexadas ao seu executável durante a etapa de compilação (na etapa do linker).

Ótimo quando você quer que seu programa tenha toda a lógica.

.so (shared object)

Se trata de uma biblioteca compartilhada (shared library), ou seja, podemos ver como um executável que será utilizado por qualquer programa que precise dele (ainda é preciso avisar ao linker da existencia da biblioteca).

Ótimo pois ocupa menos espaço do computador da pessoa com a mesma lógica.

GCC 4 Steps

Eu já mencionei linker duas vezes, para entender o que ele é precisamos olhar para cada etapa do GCC.

4 etapas do GCC

Um resumo seria:

  • Preprocessor
    • Responsável por fazer pré processamentos, o mais popular é a substituição de #include pelo código dentro dos headers (.h).
    • A saída dessa etapa é conhecida como translation unit (ainda é código C).
  • Compiler
    • Responsável por converter o código C para código assembly.
  • Assembler
    • Responsável por converter o código assembly para um object code, ele é formado de código de máquina (código especifico para rodar naquela máquina) e "referências" a serem encontradas.
      • Por exemplo, você pode referênciar a função void do_it() mas ela não estar neste arquivo .c.
  • Linker
    • Responsável por encontrar as referências de um arquivo e linkar elas, a saída é justamente o executável final.

Project from Zero

Começamos com uma estrutura bem vazia de projeto:

project/
└── src/
├── main.c
└── ...

Apens possuimos código nosso, então tudo que precisamos fazer para criar o executável é

gcc src/main.c -o main

Algum momento do nosso projeto decidimos usar bibliotecas de terceiro e não queremos misturar ela com o nosso código então resolvemos sempre separar os arquivos dela em outros diretórios.

O primeiro tipo de arquivo que bibliotecas trazem junto são os headers, utilizados justamente para o compilador conseguir determinar o que exatamente tem que buscar nas bibliotecas.

project/
├── include/
│ └── header.h
└── src/
├── main.c
└── ...

Podemos adicionar um diretório onde se buscar headers com o argumento -I seguido pelo diretório (não é separado por espaço):

gcc src/main.c -o main -Iinclude

Agora podemos adicionar #include <header.h> sem o compilador reclamar que header.h não foi encontrado.

Se tentarmos chamar uma função que está no header.h ainda teremos erro pois não temos a função, apenas a assinatura dela. Nós precisamos do código da função, que pode estar em um .so ou .a.

warning
  • Saiba se sua biblioteca é C++ ou C para saber se deve usar gcc ou g++.
  • Se ambos tipos de bibliotecas existirem o linker talvez priorize o .so.

.a

Novamente não vamos querer misturar este arquivo com os do nosso projeto, então vamos deixar a biblioteca na pasta lib.

project/
├── include/
│ └── header.h
├── lib/
│ └── libname.a
└── src/
├── main.c
└── ...

Podemos passar ao compilador diretório onde as nossas bibliotecas se encontram com o argumento -L seguido pelo diretório (não é separado por espaço):

gcc src/main.c -o main -Iinclude -Llib

Precisamos especificar as que desajamos usar e isso é feito com o argumento -l seguido pelo nome base da biblitoeca (não é separado por espaço):

gcc src/main.c -o main -Iinclude -Llib -lname
note

-lname vai buscar pela biblioteca libname.a, este é um atalho para referênciar bibliotecas.
-l:libname.so pode ser usado em casos que o nome da biblioteca não segue estes padrões.


.so

Segue basicamente a mesma lógica do .a, deixar a biblioteca no diretório lib.

project/
├── include/
│ └── header.h
├── lib/
│ └── libname.so
└── src/
├── main.c
└── ...

Passar ao compilador diretório onde as nossas bibliotecas se encontram com o argumento -L seguido pelo diretório (não é separado por espaço):

gcc src/main.c -o main -Iinclude -Llib

No caso de bibliotecas compartilhadas este diretório pode possuir centenas de bibliotecas, então faz sentido você ter que especificar as que você quer usar. Novamente usamos o argumento -l seguido pelo nome base da biblitoeca (não é separado por espaço):

gcc src/main.c -o main -Iinclude -Llib -lname
note

-lname vai buscar pela biblioteca libname.so, este é um atalho para referênciar bibliotecas.
-l:libname.so pode ser usado em casos que o nome da biblioteca não segue estes padrões.

Durante a execução do código o sistema operacional vai buscar a biblioteca em lugares predefinidos (linux busca em lugares como /usr/lib).

Mas se nossa biblioteca não for estar em um destes lugares predefinidos? Ainda é possível adicionar lugares onde se buscar durante a execução.

Podemos passar argumentos ao linker com -Wl seguido pelos argumentos que ele deve receber, no caso -Rlib (argumentos separados por virgula):

gcc src/main.c -o main -Iinclude -Llib -lname -Wl,-Rlib

Nosso executável agora vai sempre tentar buscar a biblioteca na pasta lib que estiver no mesmo diretório que ele.

References

Thiago Lages de Alencar

Dado que já refleti sobre forward kinematics, está na hora de falar sobre inverse kinematics (por mais que eu esteja com preguiça de fazer isso).

Vamos começar por algo que eu até me questiono se seria inverse kinematic: Look at.

Ser capaz de fazer uma mão apontar para uma posição ou a cabeça olhar para uma direção.

Jogador 2D movendo a cabeça para cima

Look At

Lembrando que articulações sempre possuem as suas informações locais e que as informações globais são facilmente calculáveis, nossa tarefa é descobrir como queremos alterar qualquer uma delas para alcançar nosso objetivo.

No caso, nosso objetivo é fazer com que o vetor da articulação aponte para X.

Uma imagem com o vector apontando para um X na direita dele e outra imagem igual porém com o X movido para cima do vetor

Na primeira parte da imagem, temos o vetor já apontando em direção do X.
Na segunda parte da imagem, o X se encontra a 90º graus do vetor.

Olhando a imagem nós conseguimos saber que para continuar apontando para X temos que rotacionar por 90º, mas como conseguir este ângulo matemáticamente?

Talvez você já tenha notado mas vamos fazer isto usando trigonometria (se prepare que IK é triângulo para tudo que é lado).

0º ~ 90º

Um bom começo é sabermos calcular o ângulo para uma posição, sem se preocupar com detalhes como global e local.

Vetor com ângulo desconhecido entre 0º e 90º

Funções trigonométricas são o segredo para trabalhar com triângulos (seno, cosseno e tangente) e neste caso tangente é justamente o que procuramos.

tan θ = cateto oposto / cateto adjacente

Agora nós temos o valor da tangente para qualquer posição (x,y).

  • (1,1)
    • tan(θ) = 1
  • (3,2)
    • tan(θ) = 2/3
  • (4,7)
    • tan(θ) = 7/4

Se você bem se lembra, existem funções trigonométricas inversas que são justamente quem vão nos dar o ângulo que chega ao valor que temos.

  • (1,1)
    • tan(θ) = 1
      • atan(1) = θ
        • 45
  • (3,2)
    • tan(θ) = 2/3
      • atan(2/3) = θ
        • 33.690067526
  • (4,7)
    • tan(θ) = 7/4
      • atan(7/4) = θ
        • 60.255118703

0º ~ 360º

Um problema que cedo ou tarde iriamos notar é que dos valores da tangete não é possível definir qual quadrante se trata.

Imagem mostrando que não da pra decompor o ângulo a partir do valor da tangente

atan(1) pode ser 45º ou 225º
atan(-1) pode ser 135º ou 315º

Única maneira de saber exatamente o quadrante é sabendo o sinal dos eixos.

  • (positivo,positivo)
    • atan(θ) + 0º
  • (negativo,positivo)
    • atan(θ) + 90º
  • (negativo,negativo)
    • atan(θ) + 180º
  • (positivo,negativo)
    • atan(θ) + 270º

É por isso que em muitas bibliotecas matemáticas existe a função atan(v) e a função atan2(x, y).
A segunda utiliza os eixos para saber o real ângulo.

Rotating

Agora que sabemos como obter o ângulo do ponto (0,0) até uma posição qualquer, podemos finalmente rotacionar a articulação.

O próximo problema é que não estamos falando de rotacionar a partir do ponto (0,0) mas sim da posição da articulação.

Mostrando diferença entre o ângulo local e global

θ1: Rotação global, usando ponto (0,0) como referência.
θ2: Rotação local, usando a posição da articulação como referência.

Para solucionar isto podemos calcular a posição do ponto em relação a articulação:

posição do ponto relativa à articulação =
posição global do ponto - posição global da articulação

Exemplo:

Posição global do ponto: (35, 10)
Posição global da articulação: (25, 10)
Posição do ponto relativa à articulação: (10, 0)
Ângulo: 0º

Se movermos o ponto:

Posição global do ponto: (50, 30)
Posição global da articulação: (25, 10)
Posição do ponto relativa à articulação: (25, 20)
Ângulo: 38º

Pronto, agora sabemos qual deveria ser a rotação daquela articulação!

Conclusion

Você provavelmente não terá que pensar em nada disso pois muitas game engines já possuem métodos para lidar com isto, por exemplo em Godot podemos encontrar algo como:

Node2D.get_angle_to(global_position)

Essa função retorna o ângulo global que falta para estar em direção ao ponto global.

References