Kod Ne Kadar Doğal?

Merhabalar.

Henüz yeni C öğrenmeye başladım. Low-level programlama adına Rust’tan öteye hiç gitmedim daha önce. Yani özellikle memory management konusunda öğreneceğim çok şey var.

Pratik olması açısından lichess.org’da zaman kontrollerine göre en iyi 10 satranç oyuncularını gösteren bir program yazmak istedim. Github reposu üzerinden kodları okumak isterseniz: Github linki

Şimdi neler yaptığımı ve neden bu konuyu açtığımı anlatayım.
Aşağıdaki kod fetch_players.h:

#ifndef FETCH_PLAYERS_H
#define FETCH_PLAYERS_H
#define API "https://lichess.org/api/player"

void fetch_top_players();

#endif

fetch_players.c:
main.c’de bu modülün içindeki fetch_top_players() fonksiyonunu çağırıp lichess’te zaman kontrollerine göre en yüksek elolu satranç oyuncularını printlemek istiyorum.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <curl/curl.h>
#include "fetch_players.h"
#include "json-parser/parser.h"

size_t write_callback(void *incoming_data, size_t size_of_each_unit, size_t number_of_units, void *user_provided_buffer)
{
    size_t total_data_size = size_of_each_unit * number_of_units;
    char **response_buffer = (char **)user_provided_buffer;

    if (!(*response_buffer))
    {
        *response_buffer = (char *)malloc(total_data_size + 1);
        if (!(*response_buffer))
        {
            fprintf(stderr, "Memory allocation failed\n");
            return 0;
        }
    }

    memcpy(*response_buffer, incoming_data, total_data_size);
    (*response_buffer)[total_data_size] = '\0';

    return total_data_size;
}

void fetch_top_players()
{
    CURL *curl = NULL;
    CURLcode res;
    char *response_buffer = NULL;

    curl_global_init(CURL_GLOBAL_DEFAULT);

    curl = curl_easy_init();

    if (curl)
    {
        curl_easy_setopt(curl, CURLOPT_URL, API);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_buffer);

        res = curl_easy_perform(curl);

        if (res != CURLE_OK)
        {
            fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        }

        curl_easy_cleanup(curl);
    }

    curl_global_cleanup();

    if (response_buffer)
    {
        parse_json_response(response_buffer);
        free(response_buffer);
    }
}

Bir de parser.c var:
Burada da json response’u parse edip oyuncuları sıralamayı planlıyorum. Şimdilik sadece argüman olarak verilen json response’u print ediyor. Daha sonra geliştirmeye çalışacağım burayı.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include "parser.h"

void parse_json_response(char *json)
{
    printf("Response value:\n");
    printf("----\n");
    printf("%s\n", json);
}

fetch_players.c'de curl kullanarak lichess apisine request atıyorum. Yalnız buradaki gibi bir curl kodu sadece request atıp response’u terminalde göstermeye yarıyor. Ben de response’u daha sonra parse_json_response() gibi fonksiyonlar yazıp işleyebilmek için bunu bir değişkene aktarmam gerektiğini düşündüm.

Bu noktada tıkanınca yapay zekadan kod istedim. Bir callback function yazdı: write_callback(). Sonra da bu fonksiyondan yararlanarak curl’den gelen response’u bir buffer’da tutabilmek için bu kodu yazdı:

curl_easy_setopt(curl, CURLOPT_URL, API);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_buffer);

Bu sayede artık response bir buffer’da (tampon?) tutulduğu için artık onu bir string olarak işleyebilecektik. Her şey gayet mantıklı görünüyor.


Kodun mantığını aşağı yukarı anladım. Sadece
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
burada curl_easy_setopt()'nin write_callback()'i nasıl kullandığını biraz merak ediyorum. Neyse, asıl konu bu değil.

Şimdi yapay zeka (chatgpt) mantıklı ve işe yarayan bir çözüm sunmuş gibi görünüyor, kodun mantığını (sonuç olarak neyi sağladığını kastediyorum) da anladım genel olarak ancak kodun işleyişi hakkında, kodun kendisi hakkında soru işaretlerim oldu.

size_t write_callback(void *incoming_data, size_t size_of_each_unit, size_t number_of_units, void *user_provided_buffer)
{
    size_t total_data_size = size_of_each_unit * number_of_units;
    char **response_buffer = (char **)user_provided_buffer;

    if (!(*response_buffer))
    {
        *response_buffer = (char *)malloc(total_data_size + 1);
        if (!(*response_buffer))
        {
            fprintf(stderr, "Memory allocation failed\n");
            return 0;
        }
    }

    memcpy(*response_buffer, incoming_data, total_data_size);
    (*response_buffer)[total_data_size] = '\0';

    return total_data_size;
}

Yeni yeni C öğrendiğimi de düşünürsek bu kodu okumakta epey zorlandım doğrusu. Pek insansı bir kod gibi de durmuyor sanki, pek doğal durmuyormuş gibi. Ya da idiomatic C kodu değilmiş gibi. Yeni öğrenen biri olarak bunu söylemem pek de mantıklı değil elbette ama bir şekilde şüphe duyuyorum sadece. Bu arada yapay zeka başta statik bir buffer tanımlamıştı char[4096] gibi. Stack smash... gibi bir hata yükseltilmişti. Anlaşılan o ki buffer’ın genişliği biraz az gelmiş. Daha sonra dynamic memory management yapan bir program için promptlar atınca şu anki halini aldı.

Sonuç olarak kod karmaşık geldiği için foruma bir danışmak istedim. Curl’den gelen response’u işlemek için yapay zekanın yaptığı gibi buffer tutmak iyi bir practice mi yoksa aynı mantığı daha insansı ve idiomatic bir şekilde işlemek mümkün mü? Mümkünse nasıl bir şey yazılabilir? Ve ayrıca yapay zekanın yazdığı kodu da benim için detaylı bir şekilde analiz edebilir misiniz? Son olarak repoda ayrıca iyileştirebileceğim bir şeyler varsa bunlardan da bahsederseniz memnun olurum. Makefile’ımı da yapay zekaya yazdırmıştım mesela ama belki de olması gerekenden daha gereksiz bir şekilde karmaşık olmuştur basit bir repo için.

Doğal/insansı ya da idiomatic koddan kastım bazen yapay zekanın basit bir iş için olağandan daha karmaşık şeyler yazması ile ilgili. İnsanın aklına ilk gelen genelde daha sade, daha basit bir yöntem olabiliyor. Yapay zeka ise olur olmadık işleri karmaşıklaştırabiliyor. Satrançta insanın kolaylıkla kazanca giden yolu seçerken yapay zekanın ortada basit bir kazanç yolu olmasına rağmen en en iyi yoldan kazanmayı karmaşıklaştırması gibi bir benzerlik kurulabilir belki de. Yani amacım daha çok kod hakkında analizlerinizi görüp kendimi geliştirmek; aynı amaca hizmet eden daha iyi bir yöntem varsa bunu öğrenmek, şu anki kod yeterince iyiyse de tam olarak nasıl çalıştığını anlamak. Pointerlar, memory işlemleri vb. low level konseptlerde henüz acemi sayılırım. Bunu da göz önünde bulundurunca sizlerin bilgilerinden istifade etmek istedim.

Not: Konu için aklınıza daha iyi bir başlık gelirse değiştirebilirsiniz.

1 Beğeni

Birden çok soru var ama hepsini uzunca cevaplamak gerekir.

Basit bir soru ile başlayalım.

Bir bellek ayırırken sabit bir bellek ayırmak ve dinamik bellek ayırmak.

İkisi de ihtiyaca göre kullanılabilir.

Gelen verinin max boyutunu biliyorsanız sabit bellek ayırmak mantıklı, ama gelecek verinin boyutunu kestiremiyor ve daha etkin bellek yönetimi istiyorsanız dinamik bellek yönetimi mantıklı.

Tabi burada derleyici ve işletim sisteminizin size ne kadar izin verdiği de ayrı bir konu.

Sonuç olarak dinamik bellek ayırarak kullanmakta bir tuhaflık göremiyorum.

Burada bellek ayrılıp ayrılmadığını kontrol etmek bellek taşmaları gibi sorunların bir nebze önüne geçebilir. Ama çok gerekli görünmüyor bellek ayırılmış başarımını kontrol eden ve memory allocation failed uyarısı veren bir kontrol yapılmış ki bu da yeter.

Diğer sorular için bir kod farklı yüzlerce şekilde yazılabilir. Eleştirilebilir değiştirilebilir.

Ama konu C olunca zaten minimal kod yazdırıyor ve çok eleştirmeye gerek bırakmıyor.

Eleştirilecek konular sadece fonksiyon seçimleri, isimleri, dönüş parametrelerii dönüş parametreleri gibi tercihlerle alakalı olabilir.

Buna da yiğidin yoğurt yiyişi denip görmezden gelinebilir.

Yani göze batan kod diyemem ama bellek yönetimi ve bellek taşmaları ile hata yönetimi konularında gözden geçirilebilir.

Teşekkür ederim öncelikle.
Peki burada mesela ne oluyor tam olarak?

Ya da

buradaki **'ler ne ifade ediyor? Pointerların mantığını biliyorum aşağı yukarı ama üstte tam olarak ne olduğundan emin değilim.

Ya da

burada malloc ne yapıyor tam olarak? Yani sonuç olarak memory’de buffer için ihtiyaç olan miktarda bir alan açacak elbette ama bunu nasıl yapıyor? Memory çok soyut bir kavram şu anda benim için. O yüzden “kod şu işi yapıyor” dendiği zaman tam olarak anlamış gibi hissetmiyorum.

image
Rust book’ta üstteki gibi şemalarla çeşitli somutlaştırmalar olurdu mesela. Bu tür şeyler biraz daha faydalı oluyor bir şeyleri daha iyi idrak edebilmek için.

malloc tanımından gidelim.

malloc - cppreference.com

void *malloc( size_t size );

malloc size belleğe bir işaretçi sağlıyor.

Peki siz bu işaretçiyi bir bir işaretçi değişkende tutmak isterseniz ne olur?

Buradaki gibi;

Yani siz size bir işaretçi döndüren malloctan aldığınız işaretçiyi bir buffer işaretçisine atayaaksanız, işaretçiye işaretçi yani pointer to pointer yaparsınız.

Tabi işaretçiye işaretçiyi ayırabilmesi için ** iki kez kullanılmış.

C - Pointer to Pointer (tutorialspoint.com)

Yukarıdaki linkten pointer to pointer konusuna göz atabilirsiniz.

Sorular yerinde cevapları özet geçiyorum.

Detaylandırmamı istediğiniz yer olursa üzerinde dururuz.

1 Beğeni

user_provided_buffer standart bir cikti parametresi olarak kullanilmis.

Dinamik allocate edilmis bir alana pointer donduren fonksiyondan once int donduren bir fonksiyon yazmak gerekiyor. Bes adim birden atlayinca “AI ne yapmis burada” oluyor boyle—yazdigi kod dogru ise bile.

Akis olarak son derece idiomatic ve temiz bir C kodu. Stil olarak kotu bir C veya C++ kodu. Ful incelemesini yapmistim ama yazanin insan olmadigini fark edince sildim.

malloc’u hafiza alani donduren bir kara kutu olarak dusunmekte hic bir sakinca yok.

O zaman bu tur seyler cizmenizi tavsiye ediyorum. Buraya koyarsaniz uzerinden tartisilinabilir de.

1 Beğeni

Nasıl bir fonksiyon yazıyoruz, neyi amaçlıyoruz? Belki ipucu olsun diye ufak bir pseudo code da gösterebilirsiniz. Kodu iyileştirmek için kendim bir şeyler denemek isterim.

Neden akış için temiz ama stil için kötü? Üstte dediğiniz gibi pointer döndüren bir fonksiyondan önce integer döndüren bir fonksiyon yazarsak stil olarak da iyileştirmiş olur muyuz?

void sayi_topla(int x, int y, int *z);

x ve y girdi, z cikti.

Cunku stil C++ bilen birinin C yazmaya calismasi. void *'ler cast edilmis, fonksiyonlarin parametre listeleri unutulmus (hede()). Akis temiz cunku C++ bilen biri yazmis :).

Stil olarak iyilestirmek icin C ogrenmek lazim. C++'in destekledigi C diyalektini kullaninca boyle oluyor.

1 Beğeni

Merhabalar. C’ye nereden ve nasıl çalışıyorsunuz acaba? Bende C’de kendimi geliştirmeye çalışıyorum. Bana verebileceğiniz herhangi bir tavsiye veya fikir varsa hepsine açığım. Şimdiden teşekkürler.

Fikir nedir burada? Neden böyle bir şey yazdığımızı anlayamadım :thinking:

Bir de ben böyle bir örneğe rastladım biraz önce. Curl’ün getirdiği datayı buffer’da tutmak için yazılmış, tam olarak benim de yapmaya çalıştığım şeyi yapıyor.

Oradaki kodu şöyle toparladım kendimde:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <curl/curl.h>
#include "fetch_players.h"
#include "json-parser/parser.h"

typedef struct
{
    char *memory;
    size_t size;
} MemoryStruct;

static size_t write_callback(void *incoming_data, size_t size_of_each_unit, size_t number_of_units, void *user_provided_buffer)
{
    size_t total_data_size = size_of_each_unit * number_of_units;
    MemoryStruct *mem = (MemoryStruct *)user_provided_buffer;

    mem->memory = realloc(mem->memory, mem->size + total_data_size + 1);

    if (mem->memory == NULL)
    {
        printf("Not enough memory");
        return 0;
    }

    memcpy(&(mem->memory[mem->size]), incoming_data, total_data_size);
    mem->size += total_data_size;
    mem->memory[mem->size] = 0;
    return total_data_size;
}

void fetch_top_players()
{
    CURL *curl;
    CURLcode res;

    MemoryStruct chunk;

    chunk.memory = malloc(1);
    chunk.size = 0;

    curl_global_init(CURL_GLOBAL_ALL);
    curl = curl_easy_init();

    curl_easy_setopt(curl, CURLOPT_URL, API);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);

    res = curl_easy_perform(curl);
    if (res != CURLE_OK)
    {
        fprintf(stderr, "curl failed\n");
    }

    curl_easy_cleanup(curl);

    if (chunk.memory)
    {
        parse_json_response(chunk.memory);
        free(chunk.memory);
    }

    curl_global_cleanup();
}

Bu şekilde yazarsak kodun stil açısından da iyi olduğunu söyleyebilir miyiz? Öncekinden farklı olarak daha az type casting var (C’de genel olarak gereksiz casting’lerden kaçınmak iyi bir practice’miş sanırım) ve MemoryStruct ile kod biraz daha organize görünüyor gibi (öncekinde double pointer’lar vs. biraz daha karmaşık gözüküyordu.)


@marjin C öğrenmeye çalıştığım direkt bir kaynak yok. Aklıma esen ufak ufak practice’leri yaparak öğrenmeye çalışıyorum. Sırf şu anda yapmaya çalıştığım program bile bir sürü şey öğretiyor bana. Bir de vakit bulabilsem güzel olacak elbette. Hafta sonu hariç vakit ayırmak zor oluyor okuldan müt.

Surada bir tane kalmis.

Yazmadik ki.
Belki yazip bir sonraki adima gecsek anlardik.

curl’den request edilen bir sayiyi alalim mesela
https://www.random.org/integers/?num=1&min=1&max=100&col=1&base=10&format=plain

1 Beğeni

Ben denedim bir şeyler ama sonraki adıma geçtik mi bilmiyorum. Bayağı kafam karıştı.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <curl/curl.h>

#define URL "https://www.random.org/integers/?num=1&min=1&max=100&col=1&base=10&format=plain"

// https://everything.curl.dev/libcurl/callbacks/write
size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
{
   ? ? ?
}

char *curl_request(char api_url[])
{
   CURL *curl;
   CURLcode *res;
   char *buffer;

   curl_global_init(CURL_GLOBAL_ALL);
   curl = curl_easy_init();

   curl_easy_setopt(curl, CURLOPT_URL, api_url);
   curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
   curl_easy_setopt(curl, CURLOPT_WRITEDATA, buffer);

   res = curl_easy_perform(curl);
   if (res != CURLE_OK)
   {
      fprintf(stderr, "Curl failed\n");
      free(buffer);
      return "";
   }

   curl_easy_cleanup(curl);
   curl_global_cleanup();

   return buffer;
}

void sayi_topla(int x, int y, int *z)
{
   z = x + y;
   printf("%d", z);
}

int main()
{
   char num_1 = curl_request(URL);
   char num_2 = curl_request(URL);

   num_1 = atoi(num_1);
   num_2 = atoi(num_2);

   sayi_topla(num_1, num_2);

   return 0;
}

AI vs. kullanmadım, sıfırdan kendim yazıp anlamaya çalışıyorum. Sonraki adımın bir parçası da bu olsa gerek. Birtakım yönlendirmelerle mevzuyu kendi kendime çözdürmeye çalışıyorsunuz sanırım :slight_smile:

Sunun hata vermesi lazim cunku z int degil.

Bunun uc parametre almasi lazim.

cURL’u simdilik karistirmayalim, o 2. odev :slight_smile:
int num_1 = 1; int num_2 = 2;

Yeni baslik acalim isterseniz? Daha temiz, duzgun yazarim.

@marjin C pratigi yapmak istiyordu, o da takip edebilir.

3 Beğeni

Bence harika olur, hayır diyemem :slight_smile:

Hm, o zaman şöyle bir şey?

#include <stdio.h>

void sayi_topla(int x, int y, int *z)
{
   *z = x + y;
}

int main()
{
   int num_1 = 1;
   int num_2 = 2;
   int result;

   sayi_topla(num_1, num_2, &result);

   printf("%d\n", result); // bu sefer int :D

   return 0;
}

Olur tabii.